diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c83ce7c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+**/*.bundle.js
+**/*.js.map
+**/coverage
+**/files
+**/node_modules/*
+**/dist
+**/vendor
+**/yarn-error
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..fa9ad2d
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,38 @@
+builds:
+ -
+ main: server.go
+ env:
+ - CGO_ENABLED=0
+ binary: quickshare
+ goos:
+ - windows
+ - darwin
+ - linux
+ goarch:
+ - amd64
+archive:
+ name_template: "quickshare_{{ .Tag }}_{{ .Os }}_{{ .Arch }}"
+ format: zip
+ wrap_in_directory: true
+ replacements:
+ darwin: macos
+ linux: linux
+ windows: windows
+ amd64: x86_64
+ files:
+ - LICENSE
+ - README.md
+ - config.json
+ - public/*
+ - public/dist/*
+ - docs/*
+checksum:
+ name_template: 'checksums.txt'
+snapshot:
+ name_template: "{{ .Tag }}-next"
+changelog:
+ sort: asc
+ filters:
+ exclude:
+ - '^docs:'
+ - '^test:'
diff --git a/Gopkg.lock b/Gopkg.lock
new file mode 100644
index 0000000..03afe0c
--- /dev/null
+++ b/Gopkg.lock
@@ -0,0 +1,21 @@
+# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
+
+
+[[projects]]
+ name = "github.com/pkg/errors"
+ packages = ["."]
+ revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
+ version = "v0.8.0"
+
+[[projects]]
+ name = "github.com/robbert229/jwt"
+ packages = ["."]
+ revision = "81ddea8e91eecffef557c5e4ce8e78a1d472d7d7"
+ version = "v2.0.0"
+
+[solve-meta]
+ analyzer-name = "dep"
+ analyzer-version = 1
+ inputs-digest = "09f88c8b25db4caace633bcccce4f4d942a810603dd2befe65a69191b0869b14"
+ solver-name = "gps-cdcl"
+ solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
new file mode 100644
index 0000000..979f33a
--- /dev/null
+++ b/Gopkg.toml
@@ -0,0 +1,34 @@
+# Gopkg.toml example
+#
+# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
+# for detailed Gopkg.toml documentation.
+#
+# required = ["github.com/user/thing/cmd/thing"]
+# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
+#
+# [[constraint]]
+# name = "github.com/user/project"
+# version = "1.0.0"
+#
+# [[constraint]]
+# name = "github.com/user/project2"
+# branch = "dev"
+# source = "github.com/myfork/project2"
+#
+# [[override]]
+# name = "github.com/x/y"
+# version = "2.4.0"
+#
+# [prune]
+# non-go = false
+# go-tests = true
+# unused-packages = true
+
+
+[prune]
+ go-tests = true
+ unused-packages = true
+
+[[constraint]]
+ name = "github.com/robbert229/jwt"
+ version = "2.0.0"
diff --git a/README.md b/README.md
index c53d3ac..14237f7 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,69 @@
-# share-heap
-A simple file sharing server.
\ No newline at end of file
+
+ Quickshare
+
+
+ A succinct file sharing server
+
+
+
+
+
+
+
+
+
+Choose Language: English | [简体中文](./docs/README_zh-cn.md)
+
+## Download
+
+| Linux | Mac | Windows |
+| ------------ | ------------ | ------------ |
+| [Download]() | [Download]() | [Download]() |
+
+## Features
+
+* Upload and download in browser, no client
+* Share files among desktop and mobile devices
+* Portable software
+* Add files from local
+* Add download limit for resource
+* Download from interrupted point
+
+## Installation
+
+2 steps are needed to start a quickshare: unzip it and start it.
+
+The first step, unzip and start quickshare
+
+### Linux
+
+* Unzip the package: `unzip [package].` (`[package]` could be `quickshare_0.0.8_linux_x86_6 4.zip`)
+* Start quickshare `./quickshare`
+
+### Mac
+
+* Unzip the package: `unzip [package].` (`[package]` could be `quickshare_0.0.8_macos_x86_64.zip`)
+* Start quickshare `./quickshare`
+
+### Windows
+
+* Unzip the package
+* Go into folder and click `quickshare.exe`
+
+Last step, meet quickshare in browser
+
+* Quickshare will start and show `quickshare starts @ [URL]` in terminal (e.g. `URL` could be `192.168.0.1:8888`)
+* Open `URL` in browser and login with `admin` and `quicksh@re`
+* Enjoy (But don't forget to change password according to [FAQ document](./docs/FAQ_en-us.md))
+
+### FAQ
+
+Please refer [FAQ document](./docs/FAQ_en-us.md)
+
+### Configuration
+
+Please refer [Configuration document](./docs/CONFIG_en-us.md)
+
+### Contribution
+
+Will add it soon...
diff --git a/client/.babelrc b/client/.babelrc
new file mode 100644
index 0000000..de40a79
--- /dev/null
+++ b/client/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["env", "react", "stage-0", "stage-2"]
+}
diff --git a/client/components/composite/auth_pane.jsx b/client/components/composite/auth_pane.jsx
new file mode 100644
index 0000000..0ba99df
--- /dev/null
+++ b/client/components/composite/auth_pane.jsx
@@ -0,0 +1,145 @@
+import React from "react";
+import { Button } from "../control/button";
+import { Input } from "../control/input";
+
+import { config } from "../../config";
+import { getIcon } from "../display/icon";
+import { makePostBody } from "../../libs/utils";
+import { styleButtonLabel } from "./info_bar";
+
+export const classLogin = "auth-pane-login";
+export const classLogout = "auth-pane-logout";
+const IconSignIn = getIcon("signIn");
+const IconSignOut = getIcon("signOut");
+const IconAngRight = getIcon("angRight");
+
+export class AuthPane extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ adminId: "",
+ adminPwd: ""
+ };
+ }
+
+ onLogin = e => {
+ e.preventDefault();
+ this.props.onLogin(
+ this.props.serverAddr,
+ this.state.adminId,
+ this.state.adminPwd
+ );
+ };
+
+ onLogout = () => {
+ this.props.onLogout(this.props.serverAddr);
+ };
+
+ onChangeAdminId = adminId => {
+ this.setState({ adminId });
+ };
+
+ onChangeAdminPwd = adminPwd => {
+ this.setState({ adminPwd });
+ };
+
+ render() {
+ if (this.props.isLogin) {
+ return (
+
+ }
+ label={"Logout"}
+ styleLabel={styleButtonLabel}
+ styleDefault={{ color: "#666" }}
+ styleContainer={{ backgroundColor: "#ccc" }}
+ />
+
+ );
+ } else {
+ if (this.props.compact) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ }
+ }
+}
+
+AuthPane.defaultProps = {
+ onLogin: () => console.error("undefined"),
+ onLogout: () => console.error("undefined"),
+ compact: false,
+ isLogin: false,
+ serverAddr: "",
+ styleContainer: {},
+ styleStr: ""
+};
diff --git a/client/components/composite/file_box.jsx b/client/components/composite/file_box.jsx
new file mode 100644
index 0000000..a19f513
--- /dev/null
+++ b/client/components/composite/file_box.jsx
@@ -0,0 +1,249 @@
+import React from "react";
+import { FileBoxDetail } from "./file_box_detail";
+import { Button } from "../control/button";
+
+import { config } from "../../config";
+import { getIcon, getIconColor } from "../display/icon";
+import { getFileExt } from "../../libs/file_type";
+import { del, publishId, shadowId, setDownLimit } from "../../libs/api_share";
+
+const msgUploadOk = "Uploading is stopped and file is deleted";
+const msgUploadNok = "Fail to delete file";
+
+const styleLeft = {
+ float: "left",
+ padding: "1rem 0 1rem 1rem"
+};
+
+const styleRight = {
+ float: "right",
+ textAlign: "right",
+ padding: "0rem"
+};
+
+const clear = ;
+
+const iconDesStyle = {
+ display: "inline-block",
+ fontSize: "0.875rem",
+ lineHeight: "1rem",
+ marginBottom: "0.25rem",
+ maxWidth: "12rem",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ textDecoration: "none",
+ verticalAlign: "middle",
+ whiteSpace: "nowrap"
+};
+
+const descStyle = {
+ fontSize: "0.75rem",
+ padding: "0.75rem"
+};
+
+const otherStyle = `
+.main-pane {
+ background-color: rgba(255, 255, 255, 1);
+ transition: background-color 0.1s;
+}
+.main-pane:hover {
+ background-color: rgba(255, 255, 255, 0.85);
+ transition: background-color 0.1s;
+}
+
+.show-detail {
+ opacity: 1;
+ height: auto;
+ transition: opacity 0.15s, height 0.5s;
+}
+
+.hide-detail {
+ opacity: 0;
+ height: 0;
+ overflow: hidden;
+ transition: opacity 0.15s, height 0.5s;
+}
+
+.main-pane a {
+ color: #333;
+ transition: color 1s;
+}
+.main-pane a:hover {
+ color: #3498db;
+ transition: color 1s;
+}
+`;
+
+const IconMore = getIcon("bars");
+const IconTimesCir = getIcon("timesCir");
+const styleIconTimesCir = {
+ color: getIconColor("timesCir")
+};
+
+const iconMoreStyleStr = `
+.file-box-more {
+ color: #333;
+ background-color: #fff;
+ transition: color 0.4s, background-color 0.4s;
+}
+
+.file-box-more:hover {
+ color: #000;
+ background-color: #ccc;
+ transition: color 0.4s, background-color 0.4s;
+}
+`;
+
+let styleFileBox = {
+ textAlign: "left",
+ margin: "1px 0px",
+ fontSize: "0.75rem"
+};
+
+const styleButtonContainer = {
+ width: "1rem",
+ height: "1rem",
+ padding: "1.5rem 1rem"
+};
+
+const styleButtonIcon = {
+ lineHeight: "1rem",
+ height: "1rem",
+ margin: "0"
+};
+
+export class FileBox extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ }
+
+ onToggleDetail = () => {
+ this.props.onToggleDetail(this.props.id);
+ };
+
+ onDelete = () => {
+ del(this.props.id).then(ok => {
+ if (ok) {
+ this.props.onOk(msgUploadOk);
+ this.props.onRefresh();
+ } else {
+ this.props.onError(msgUploadNok);
+ }
+ });
+ };
+
+ render() {
+ const ext = getFileExt(this.props.name);
+ const IconFile = getIcon(ext);
+ const IconSpinner = getIcon("spinner");
+
+ const styleIcon = {
+ color: this.props.isLoading ? "#34495e" : getIconColor(ext)
+ };
+
+ styleFileBox = {
+ ...styleFileBox,
+ width: this.props.width
+ };
+
+ const fileIcon = this.props.isLoading ? (
+
+ ) : (
+
+ );
+
+ const opIcon = this.props.isLoading ? (
+ }
+ label=""
+ styleContainer={styleButtonContainer}
+ styleIcon={styleButtonIcon}
+ onClick={this.onDelete}
+ />
+ ) : (
+ }
+ className={"file-box-more"}
+ label=""
+ styleContainer={styleButtonContainer}
+ styleIcon={styleButtonIcon}
+ styleStr={iconMoreStyleStr}
+ onClick={this.onToggleDetail}
+ />
+ );
+
+ const downloadLink = (
+
+ {this.props.name}
+
+ );
+
+ const classDetailPane =
+ this.props.showDetailId === this.props.id &&
+ this.props.uploadState === "done"
+ ? "show-detail"
+ : "hide-detail";
+
+ return (
+
+
+
{fileIcon}
+
+ {downloadLink}
+
{`${this.props.size} ${this.props.modTime}`}
+
+
{opIcon}
+ {clear}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+FileBox.defaultProps = {
+ id: "",
+ name: "",
+ isLoading: false,
+ modTime: "unknown",
+ uploadState: "",
+ href: "",
+ width: "320px",
+ showDetailId: "",
+ downLimit: -3,
+ size: "unknown",
+ onToggleDetail: () => console.error("undefined"),
+ onRefresh: () => console.error("undefined"),
+ onError: () => console.error("undefined"),
+ onOk: () => console.error("undefined")
+};
diff --git a/client/components/composite/file_box_detail.jsx b/client/components/composite/file_box_detail.jsx
new file mode 100644
index 0000000..fff567c
--- /dev/null
+++ b/client/components/composite/file_box_detail.jsx
@@ -0,0 +1,230 @@
+import React from "react";
+import { Button } from "../control/button";
+import { Input } from "../control/input";
+
+export const classDelBtn = "file-box-pane-btn-del";
+export const classDelYes = "del-no";
+export const classDelNo = "del-yes";
+
+let styleDetailPane = {
+ color: "#666",
+ backgroundColor: "#fff",
+ position: "absolute",
+ marginBottom: "5rem",
+ zIndex: "10"
+};
+
+const styleDetailContainer = {
+ padding: "1em",
+ borderBottom: "solid 1rem #ccc"
+};
+
+const styleDetailHeader = {
+ color: "#999",
+ fontSize: "0.75rem",
+ fontWeight: "bold",
+ margin: "1.5rem 0 0.5rem 0",
+ padding: 0,
+ textTransform: "uppercase"
+};
+
+const styleDesc = {
+ overflow: "hidden",
+ whiteSpace: "nowrap",
+ textOverflow: "ellipsis",
+ lineHeight: "1.5rem",
+ fontSize: "0.875rem"
+};
+
+export class FileBoxDetail extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ downLimit: this.props.downLimit,
+ showDelComfirm: false
+ };
+
+ styleDetailPane = {
+ ...styleDetailPane,
+ width: this.props.width
+ };
+ }
+
+ onResetLink = () => {
+ return this.props.onPublishId(this.props.id).then(resettedId => {
+ if (resettedId == null) {
+ this.props.onError("Resetting link failed");
+ } else {
+ this.props.onOk("Link is reset");
+ this.props.onRefresh();
+ }
+ });
+ };
+
+ onShadowLink = () => {
+ return this.props.onShadowId(this.props.id).then(shadowId => {
+ if (shadowId == null) {
+ this.props.onError("Shadowing link failed");
+ } else {
+ this.props.onOk("Link is shadowed");
+ this.props.onRefresh();
+ }
+ });
+ };
+
+ onSetDownLimit = newValue => {
+ this.setState({ downLimit: newValue });
+ };
+
+ onComfirmDel = () => {
+ this.setState({ showDelComfirm: true });
+ };
+
+ onCancelDel = () => {
+ this.setState({ showDelComfirm: false });
+ };
+
+ onUpdateDownLimit = () => {
+ return this.props
+ .onSetDownLimit(this.props.id, this.state.downLimit)
+ .then(ok => {
+ if (ok) {
+ this.props.onOk("Download limit updated");
+ this.props.onRefresh();
+ } else {
+ this.props.onError("Setting download limit failed");
+ }
+ });
+ };
+
+ onDelete = () => {
+ return this.props.onDel(this.props.id).then(ok => {
+ if (ok) {
+ this.props.onOk("File deleted");
+ this.props.onRefresh();
+ } else {
+ this.props.onError("Fail to delete file");
+ }
+ });
+ };
+
+ render() {
+ const delComfirmButtons = (
+
+
+
+
+ );
+
+ return (
+
+
+
+
File Information
+
+
+ Name {this.props.name}
+
+
+ Size {this.props.size}
+
+
+ Time {this.props.modTime}
+
+
+
+
+
Download Link
+
+ {/* */}
+
+
+
+
+
+
+ Download Limit (-1 means unlimited)
+
+
+
+
+
+
+
Delete
+ {this.state.showDelComfirm ? (
+ delComfirmButtons
+ ) : (
+
+ )}
+
+
+
+ );
+ }
+}
+
+FileBoxDetail.defaultProps = {
+ id: "n/a",
+ name: "n/a",
+ size: "n/a",
+ modTime: 0,
+ href: "n/a",
+ downLimit: -3,
+ width: -1,
+ className: "",
+ onRefresh: () => console.error("undefined"),
+ onError: () => console.error("undefined"),
+ onOk: () => console.error("undefined"),
+ onDel: () => console.error("undefined"),
+ onPublishId: () => console.error("undefined"),
+ onShadowId: () => console.error("undefined"),
+ onSetDownLimit: () => console.error("undefined")
+};
diff --git a/client/components/composite/file_pane.jsx b/client/components/composite/file_pane.jsx
new file mode 100644
index 0000000..04cda00
--- /dev/null
+++ b/client/components/composite/file_pane.jsx
@@ -0,0 +1,154 @@
+import axios from "axios";
+import byteSize from "byte-size";
+import React from "react";
+import ReactDOM from "react-dom";
+import throttle from "lodash.throttle";
+import { Grids } from "../../components/layout/grids";
+import { Uploader } from "../../components/composite/uploader";
+import { FileBox } from "./file_box";
+import { TimeGrids } from "./time_grids";
+
+import { config } from "../../config";
+
+const msgSynced = "Synced";
+const msgSyncFailed = "Syncing failed";
+const interval = 250;
+
+export class FilePane extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ infos: [],
+ showDetailId: -1
+ };
+ this.onRefresh = throttle(this.onRefreshImp, interval);
+ this.onUpdateProgress = throttle(this.onUpdateProgressImp, interval);
+ }
+
+ componentWillMount() {
+ return this.onRefreshImp();
+ }
+
+ onRefreshImp = () => {
+ return this.props
+ .onList()
+ .then(infos => {
+ if (infos != null) {
+ this.setState({ infos });
+ this.props.onOk(msgSynced);
+ } else {
+ this.props.onError(msgSyncFailed);
+ }
+ })
+ .catch(err => {
+ console.error(err);
+ this.props.onError(msgSyncFailed);
+ });
+ };
+
+ onUpdateProgressImp = (shareId, progress) => {
+ const updatedInfos = this.state.infos.map(shareInfo => {
+ return shareInfo.Id === shareId ? { ...shareInfo, progress } : shareInfo;
+ });
+
+ this.setState({ infos: updatedInfos });
+ };
+
+ onToggleDetail = id => {
+ this.setState({
+ showDetailId: this.state.showDetailId === id ? -1 : id
+ });
+ };
+
+ getByteSize = size => {
+ const sizeObj = byteSize(size);
+ return `${sizeObj.value} ${sizeObj.unit}`;
+ };
+
+ getInfos = filterName => {
+ const filteredInfos = this.state.infos.filter(shareInfo => {
+ return shareInfo.PathLocal.includes(filterName);
+ });
+
+ return filteredInfos.map(shareInfo => {
+ const isLoading = shareInfo.State === "uploading";
+ const timestamp = shareInfo.ModTime / 1000000;
+ const modTime = new Date(timestamp).toLocaleString();
+ const href = `${config.serverAddr}/download?shareid=${shareInfo.Id}`;
+ const progress = isNaN(shareInfo.progress) ? 0 : shareInfo.progress;
+ const name = isLoading
+ ? `${Math.floor(progress * 100)}% ${shareInfo.PathLocal}`
+ : shareInfo.PathLocal;
+
+ return {
+ key: shareInfo.Id,
+ timestamp,
+ component: (
+
+ )
+ };
+ });
+ };
+
+ render() {
+ const styleUploaderContainer = {
+ width: `${this.props.colWidth}rem`,
+ margin: "auto"
+ };
+
+ const containerStyle = {
+ width: this.props.width,
+ margin: "auto",
+ marginTop: "0",
+ marginBottom: "10rem"
+ };
+
+ return (
+
+ );
+ }
+}
+
+FilePane.defaultProps = {
+ width: "100%",
+ colWidth: 20,
+ filterName: "",
+ onList: () => console.error("undefined"),
+ onOk: () => console.error("undefined"),
+ onError: () => console.error("undefined")
+};
diff --git a/client/components/composite/info_bar.jsx b/client/components/composite/info_bar.jsx
new file mode 100644
index 0000000..17083f4
--- /dev/null
+++ b/client/components/composite/info_bar.jsx
@@ -0,0 +1,250 @@
+import React from "react";
+
+import { Button } from "../control/button";
+import { Input } from "../control/input";
+import { getIcon, getIconColor } from "../display/icon";
+import { AuthPane } from "./auth_pane";
+import { rootSize } from "../../config";
+
+let styleInfoBar = {
+ textAlign: "left",
+ color: "#999",
+ marginBottom: "1rem",
+ margin: "auto"
+};
+
+const styleContainer = {
+ padding: "0.5rem",
+ backgroundColor: "rgba(255, 255, 255, 0.5)"
+};
+
+const styleLeft = {
+ float: "left",
+ width: "50%",
+ heigth: "2rem"
+};
+
+const styleRight = {
+ float: "right",
+ width: "50%",
+ textAlign: "right",
+ heigth: "2rem"
+};
+
+const styleButtonLabel = {
+ verticalAlign: "middle"
+};
+
+const IconPlusCir = getIcon("pluscir");
+const IconSearch = getIcon("search");
+const clear = ;
+
+export class InfoBar extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ filterFileName: "",
+ fold: this.props.compact
+ };
+ }
+
+ onLogin = (serverAddr, adminId, adminPwd) => {
+ this.props.onLogin(serverAddr, adminId, adminPwd);
+ };
+
+ onLogout = serverAddr => {
+ this.props.onLogout(serverAddr);
+ };
+
+ onSearch = value => {
+ // TODO: need debounce
+ this.props.onSearch(value);
+ this.setState({ filterFileName: value });
+ };
+
+ onAddLocalFiles = () => {
+ return this.props.onAddLocalFiles().then(ok => {
+ if (ok) {
+ // TODO: need to add refresh
+ this.props.onOk("Local files are added, please refresh.");
+ } else {
+ this.props.onError("Fail to add local files");
+ }
+ });
+ };
+
+ onToggle = () => {
+ this.setState({ fold: !this.state.fold });
+ };
+
+ render() {
+ styleInfoBar = { ...styleInfoBar, width: this.props.width };
+
+ if (this.props.compact) {
+ const IconMore = getIcon("bars");
+
+ const menuIcon = (
+
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+
+
+ );
+ const menuList = !this.state.fold ? (
+
+ ) : (
+
+ );
+
+ const menu = (
+
+ {menuIcon}
+ {menuList}
+
+ );
+ return (
+
+ {this.props.isLogin ? (
+ menu
+ ) : (
+
+ )}
+
{this.props.children}
+
+ );
+ }
+
+ const visitorPane = (
+
+ );
+
+ const memberPane = (
+
+
+
+ }
+ />
+
+ {clear}
+
+ );
+
+ return (
+
+
+ {this.props.isLogin ? memberPane : visitorPane}
+
+
{this.props.children}
+
+ );
+ }
+}
+
+InfoBar.defaultProps = {
+ compact: false,
+ width: "-1",
+ isLogin: false,
+ serverAddr: "",
+ onLogin: () => console.error("undefined"),
+ onLogout: () => console.error("undefined"),
+ onAddLocalFiles: () => console.error("undefined"),
+ onSearch: () => console.error("undefined"),
+ onOk: () => console.error("undefined"),
+ onError: () => console.error("undefined")
+};
diff --git a/client/components/composite/log.jsx b/client/components/composite/log.jsx
new file mode 100644
index 0000000..d91b5bc
--- /dev/null
+++ b/client/components/composite/log.jsx
@@ -0,0 +1,214 @@
+import React from "react";
+import { getIcon, getIconColor } from "../display/icon";
+
+const statusNull = "null";
+const statusInfo = "info";
+const statusWarn = "warn";
+const statusError = "error";
+const statusOk = "ok";
+const statusStart = "start";
+const statusEnd = "end";
+
+const IconInfo = getIcon("infoCir");
+const IconWarn = getIcon("exTri");
+const IconError = getIcon("timesCir");
+const IconOk = getIcon("checkCir");
+const IconStart = getIcon("refresh");
+
+const colorInfo = getIconColor("infoCir");
+const colorWarn = getIconColor("exTri");
+const colorError = getIconColor("timesCir");
+const colorOk = getIconColor("checkCir");
+const colorStart = getIconColor("refresh");
+
+const classFadeIn = "log-fade-in";
+const classHidden = "log-hidden";
+const styleStr = `
+ .log .${classFadeIn} {
+ opacity: 1;
+ margin-left: 0.5rem;
+ padding: 0.25rem 0.5rem;
+ transition: opacity 0.3s, margin-left 0.3s, padding 0.3s;
+ }
+
+ .log .${classHidden} {
+ opacity: 0;
+ margin-left: 0rem;
+ padding: 0;
+ transition: opacity 0.3s, margin-left 0.3s, padding 0.3s;
+ }
+
+ .log a {
+ color: #2980b9;
+ transition: color 0.3s;
+ text-decoration: none;
+ }
+
+ .log a:hover {
+ color: #3498db;
+ transition: color 0.3s;
+ text-decoration: none;
+ }
+`;
+
+const wait = 5000;
+const logSlotLen = 2;
+const getEmptyLog = () => ({
+ className: classHidden,
+ msg: "",
+ status: statusNull
+});
+
+const getLogIcon = status => {
+ switch (status) {
+ case statusInfo:
+ return (
+
+ );
+ case statusWarn:
+ return (
+
+ );
+ case statusError:
+ return (
+
+ );
+ case statusOk:
+ return (
+
+ );
+ case statusStart:
+ return (
+
+ );
+ case statusEnd:
+ return (
+
+ );
+ default:
+ return ;
+ }
+};
+
+export class Log extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ logs: Array(logSlotLen).fill(getEmptyLog())
+ };
+ this.id = 0;
+ }
+
+ genId = () => {
+ return this.id++ % logSlotLen;
+ };
+
+ addLog = (status, msg) => {
+ const id = this.genId();
+ const nextLogs = [
+ ...this.state.logs.slice(0, id),
+ {
+ className: classFadeIn,
+ msg,
+ status
+ },
+ ...this.state.logs.slice(id + 1)
+ ];
+
+ this.setState({ logs: nextLogs });
+ this.delayClearLog(id);
+ return id;
+ };
+
+ delayClearLog = idToDel => {
+ setTimeout(this.clearLog, wait, idToDel);
+ };
+
+ clearLog = idToDel => {
+ // TODO: there may be race condition here
+ const nextLogs = [
+ ...this.state.logs.slice(0, idToDel),
+ getEmptyLog(),
+ ...this.state.logs.slice(idToDel + 1)
+ ];
+ this.setState({ logs: nextLogs });
+ };
+
+ info = msg => {
+ this.addLog(statusInfo, msg);
+ };
+
+ warn = msg => {
+ this.addLog(statusWarn, msg);
+ };
+
+ error = msg => {
+ this.addLog(statusError, msg);
+ };
+
+ ok = msg => {
+ this.addLog(statusOk, msg);
+ };
+
+ start = msg => {
+ const id = this.genId();
+ const nextLogs = [
+ ...this.state.logs.slice(0, id),
+ {
+ className: classFadeIn,
+ msg,
+ status: statusStart
+ },
+ ...this.state.logs.slice(id + 1)
+ ];
+
+ this.setState({ logs: nextLogs });
+ return id;
+ };
+
+ end = (startId, msg) => {
+ // remove start log
+ this.clearLog(startId);
+ this.addLog(statusEnd, msg);
+ };
+
+ render() {
+ const logList = Object.keys(this.state.logs).map(logId => {
+ return (
+
+ {getLogIcon(this.state.logs[logId].status)}
+ {this.state.logs[logId].msg}
+
+ );
+ });
+
+ return (
+
+ {logList}
+
+
+ );
+ }
+}
+
+Log.defaultProps = {
+ style: {},
+ styleLog: {}
+};
diff --git a/client/components/composite/tests/auth_pane.test.jsx b/client/components/composite/tests/auth_pane.test.jsx
new file mode 100644
index 0000000..e8c00a2
--- /dev/null
+++ b/client/components/composite/tests/auth_pane.test.jsx
@@ -0,0 +1,32 @@
+import React from "react";
+import { AuthPane, classLogin, classLogout } from "../auth_pane";
+
+describe("AuthPane", () => {
+ test("AuthPane should show login pane if isLogin === true, or show logout pane", () => {
+ const tests = [
+ {
+ input: {
+ onLogin: jest.fn,
+ onLogout: jest.fn,
+ isLogin: false,
+ serverAddr: ""
+ },
+ output: classLogin
+ },
+ {
+ input: {
+ onLogin: jest.fn,
+ onLogout: jest.fn,
+ isLogin: true,
+ serverAddr: ""
+ },
+ output: classLogout
+ }
+ ];
+
+ tests.forEach(testCase => {
+ const pane = new AuthPane(testCase.input);
+ expect(pane.render().props.className).toBe(testCase.output);
+ });
+ });
+});
diff --git a/client/components/composite/tests/file_box_detail.test.jsx b/client/components/composite/tests/file_box_detail.test.jsx
new file mode 100644
index 0000000..4389082
--- /dev/null
+++ b/client/components/composite/tests/file_box_detail.test.jsx
@@ -0,0 +1,297 @@
+jest.mock("../../../libs/api_share");
+import React from "react";
+import { mount } from "enzyme";
+import { FileBoxDetail, classDelYes, classDelNo } from "../file_box_detail";
+import { execFuncs, getDesc, verifyCalls } from "../../../tests/test_helper";
+import valueEqual from "value-equal";
+import {
+ del,
+ publishId,
+ shadowId,
+ setDownLimit
+} from "../../../libs/api_share";
+
+describe("FileBoxDetail", () => {
+ test("FileBoxDetail should show delete button by default, toggle using onComfirmDel and onCancelDel", () => {
+ const box = mount();
+ expect(box.instance().state.showDelComfirm).toBe(false);
+ box.instance().onComfirmDel();
+ expect(box.instance().state.showDelComfirm).toBe(true);
+ box.instance().onCancelDel();
+ expect(box.instance().state.showDelComfirm).toBe(false);
+ });
+});
+
+describe("FileBoxDetail", () => {
+ const tests = [
+ {
+ init: {
+ id: "0",
+ name: "filename",
+ size: "1B",
+ modTime: 0,
+ href: "href",
+ downLimit: -1
+ },
+ execs: [
+ {
+ func: "onSetDownLimit",
+ args: [3]
+ }
+ ],
+ state: {
+ downLimit: 3,
+ showDelComfirm: false
+ }
+ },
+ {
+ init: {
+ id: "0",
+ name: "filename",
+ size: "1B",
+ modTime: 0,
+ href: "href",
+ downLimit: -1
+ },
+ execs: [
+ {
+ func: "onComfirmDel",
+ args: []
+ }
+ ],
+ state: {
+ downLimit: -1,
+ showDelComfirm: true
+ }
+ },
+ {
+ init: {
+ id: "0",
+ name: "filename",
+ size: "1B",
+ modTime: 0,
+ href: "href",
+ downLimit: -1
+ },
+ execs: [
+ {
+ func: "onComfirmDel",
+ args: []
+ },
+ {
+ func: "onCancelDel",
+ args: []
+ }
+ ],
+ state: {
+ downLimit: -1,
+ showDelComfirm: false
+ }
+ },
+ {
+ init: {
+ id: "0",
+ name: "filename",
+ size: "1B",
+ modTime: 0,
+ href: "href",
+ downLimit: -1
+ },
+ execs: [
+ {
+ func: "onResetLink",
+ args: []
+ }
+ ],
+ state: {
+ downLimit: -1,
+ showDelComfirm: false
+ },
+ calls: [
+ {
+ func: "onPublishId",
+ count: 1
+ },
+ {
+ func: "onOk",
+ count: 1
+ },
+ {
+ func: "onRefresh",
+ count: 1
+ }
+ ]
+ },
+ {
+ init: {
+ id: "0",
+ name: "filename",
+ size: "1B",
+ modTime: 0,
+ href: "href",
+ downLimit: -1
+ },
+ execs: [
+ {
+ func: "onShadowLink",
+ args: []
+ }
+ ],
+ state: {
+ downLimit: -1,
+ showDelComfirm: false
+ },
+ calls: [
+ {
+ func: "onShadowId",
+ count: 1
+ },
+ {
+ func: "onOk",
+ count: 1
+ },
+ {
+ func: "onRefresh",
+ count: 1
+ }
+ ]
+ },
+ {
+ init: {
+ id: "0",
+ name: "filename",
+ size: "1B",
+ modTime: 0,
+ href: "href",
+ downLimit: -1
+ },
+ execs: [
+ {
+ func: "onUpdateDownLimit",
+ args: []
+ }
+ ],
+ state: {
+ downLimit: -1,
+ showDelComfirm: false
+ },
+ calls: [
+ {
+ func: "onSetDownLimit",
+ count: 1
+ },
+ {
+ func: "onOk",
+ count: 1
+ },
+ {
+ func: "onRefresh",
+ count: 1
+ }
+ ]
+ },
+ {
+ init: {
+ id: "0",
+ name: "filename",
+ size: "1B",
+ modTime: 0,
+ href: "href",
+ downLimit: -1
+ },
+ execs: [
+ {
+ func: "onDelete",
+ args: []
+ }
+ ],
+ state: {
+ downLimit: -1,
+ showDelComfirm: false
+ },
+ calls: [
+ {
+ func: "onDel",
+ count: 1
+ },
+ {
+ func: "onOk",
+ count: 1
+ },
+ {
+ func: "onRefresh",
+ count: 1
+ }
+ ]
+ }
+ ];
+
+ tests.forEach(testCase => {
+ test(getDesc("FileBoxDetail", testCase), () => {
+ const stubs = {
+ onOk: jest.fn(),
+ onError: jest.fn(),
+ onRefresh: jest.fn(),
+ onDel: jest.fn(),
+ onPublishId: jest.fn(),
+ onShadowId: jest.fn(),
+ onSetDownLimit: jest.fn()
+ };
+
+ const stubWraps = {
+ onDel: () => {
+ stubs.onDel();
+ return del();
+ },
+ onPublishId: () => {
+ stubs.onPublishId();
+ return publishId();
+ },
+ onShadowId: () => {
+ stubs.onShadowId();
+ return shadowId();
+ },
+ onSetDownLimit: () => {
+ stubs.onSetDownLimit();
+ return setDownLimit();
+ }
+ };
+
+ return new Promise((resolve, reject) => {
+ const pane = mount(
+
+ );
+
+ execFuncs(pane.instance(), testCase.execs).then(() => {
+ pane.update();
+ if (!valueEqual(pane.instance().state, testCase.state)) {
+ return reject("FileBoxDetail: state not identical");
+ }
+
+ if (testCase.calls != null) {
+ const err = verifyCalls(testCase.calls, stubs);
+ if (err != null) {
+ return reject("FileBoxDetail: state not identical");
+ }
+ }
+
+ resolve();
+ });
+ });
+ });
+ });
+});
diff --git a/client/components/composite/tests/file_pane.test.jsx b/client/components/composite/tests/file_pane.test.jsx
new file mode 100644
index 0000000..90f0b75
--- /dev/null
+++ b/client/components/composite/tests/file_pane.test.jsx
@@ -0,0 +1,144 @@
+jest.mock("../../../libs/api_share");
+import React from "react";
+import { FilePane } from "../file_pane";
+import { mount } from "enzyme";
+import * as mockApiShare from "../../../libs/api_share";
+import { execFuncs, getDesc, verifyCalls } from "../../../tests/test_helper";
+import valueEqual from "value-equal";
+
+describe("FilePane", () => {
+ const tests = [
+ {
+ init: {
+ list: [{ Id: 0, PathLocal: "" }]
+ },
+ execs: [
+ {
+ func: "componentWillMount",
+ args: []
+ }
+ ],
+ state: {
+ infos: [{ Id: 0, PathLocal: "" }],
+ showDetailId: -1
+ },
+ calls: [
+ {
+ func: "onList",
+ count: 2 // because componentWillMount will be callled twice
+ },
+ {
+ func: "onOk",
+ count: 2 // because componentWillMount will be callled twice
+ }
+ ]
+ },
+ {
+ init: {
+ list: [{ Id: 0, PathLocal: "" }, { Id: 1, PathLocal: "" }]
+ },
+ execs: [
+ {
+ func: "componentWillMount",
+ args: []
+ },
+ {
+ func: "onUpdateProgressImp",
+ args: [0, "100%"]
+ }
+ ],
+ state: {
+ infos: [
+ { Id: 0, PathLocal: "", progress: "100%" },
+ { Id: 1, PathLocal: "" }
+ ],
+ showDetailId: -1
+ }
+ },
+ {
+ init: {
+ list: []
+ },
+ execs: [
+ {
+ func: "componentWillMount",
+ args: []
+ },
+ {
+ func: "onToggleDetail",
+ args: [0]
+ }
+ ],
+ state: {
+ infos: [],
+ showDetailId: 0
+ }
+ },
+ {
+ init: {
+ list: []
+ },
+ execs: [
+ {
+ func: "onToggleDetail",
+ args: [0]
+ },
+ {
+ func: "onToggleDetail",
+ args: [0]
+ }
+ ],
+ state: {
+ infos: [],
+ showDetailId: -1
+ }
+ }
+ ];
+
+ tests.forEach(testCase => {
+ test(getDesc("FilePane", testCase), () => {
+ // mock list()
+ mockApiShare.__truncInfos();
+ mockApiShare.__addInfos(testCase.init.list);
+
+ const stubs = {
+ onList: jest.fn(),
+ onOk: jest.fn(),
+ onError: jest.fn()
+ };
+
+ const stubWraps = {
+ onListWrap: () => {
+ stubs.onList();
+ return mockApiShare.list();
+ }
+ };
+
+ return new Promise((resolve, reject) => {
+ const pane = mount(
+
+ );
+
+ execFuncs(pane.instance(), testCase.execs).then(() => {
+ pane.update();
+ if (!valueEqual(pane.instance().state, testCase.state)) {
+ return reject("FilePane: state not identical");
+ }
+
+ if (testCase.calls != null) {
+ const err = verifyCalls(testCase.calls, stubs);
+ if (err != null) {
+ return reject(err);
+ }
+ }
+
+ resolve();
+ });
+ });
+ });
+ });
+});
diff --git a/client/components/composite/tests/info_bar.test.jsx b/client/components/composite/tests/info_bar.test.jsx
new file mode 100644
index 0000000..2c611fa
--- /dev/null
+++ b/client/components/composite/tests/info_bar.test.jsx
@@ -0,0 +1,136 @@
+jest.mock("../../../libs/api_share");
+jest.mock("../../../libs/api_auth");
+import React from "react";
+import { InfoBar } from "../info_bar";
+import { mount } from "enzyme";
+import * as mockApiShare from "../../../libs/api_share";
+import { execFuncs, getDesc, verifyCalls } from "../../../tests/test_helper";
+import valueEqual from "value-equal";
+
+describe("InfoBar", () => {
+ const tests = [
+ {
+ execs: [
+ {
+ func: "onSearch",
+ args: ["searchFileName"]
+ }
+ ],
+ state: {
+ filterFileName: "searchFileName",
+ fold: false
+ },
+ calls: [
+ {
+ func: "onSearch",
+ count: 1
+ }
+ ]
+ },
+ {
+ execs: [
+ {
+ func: "onLogin",
+ args: ["serverAddr", "adminId", "adminPwd"]
+ }
+ ],
+ state: {
+ filterFileName: "",
+ fold: false
+ },
+ calls: [
+ {
+ func: "onLogin",
+ count: 1
+ }
+ ]
+ },
+ {
+ execs: [
+ {
+ func: "onLogout",
+ args: ["serverAddr"]
+ }
+ ],
+ state: {
+ filterFileName: "",
+ fold: false
+ },
+ calls: [
+ {
+ func: "onLogout",
+ count: 1
+ }
+ ]
+ },
+ {
+ execs: [
+ {
+ func: "onAddLocalFiles",
+ args: []
+ }
+ ],
+ state: {
+ filterFileName: "",
+ fold: false
+ },
+ calls: [
+ {
+ func: "onAddLocalFiles",
+ count: 1
+ }
+ ]
+ }
+ ];
+
+ tests.forEach(testCase => {
+ test(getDesc("InfoBar", testCase), () => {
+ const stubs = {
+ onLogin: jest.fn(),
+ onLogout: jest.fn(),
+ onAddLocalFiles: jest.fn(),
+ onSearch: jest.fn(),
+ onOk: jest.fn(),
+ onError: jest.fn()
+ };
+
+ const onAddLocalFilesWrap = () => {
+ stubs.onAddLocalFiles();
+ return Promise.resolve(true);
+ };
+
+ return new Promise((resolve, reject) => {
+ const infoBar = mount(
+
+ );
+
+ execFuncs(infoBar.instance(), testCase.execs)
+ .then(() => {
+ infoBar.update();
+
+ if (!valueEqual(infoBar.instance().state, testCase.state)) {
+ return reject("state not identical");
+ }
+ if (testCase.calls != null) {
+ const err = verifyCalls(testCase.calls, stubs);
+ if (err !== null) {
+ return reject(err);
+ }
+ }
+ resolve();
+ })
+ .catch(err => console.error(err));
+ });
+ });
+ });
+});
diff --git a/client/components/composite/tests/uploader.test.jsx b/client/components/composite/tests/uploader.test.jsx
new file mode 100644
index 0000000..3922490
--- /dev/null
+++ b/client/components/composite/tests/uploader.test.jsx
@@ -0,0 +1,51 @@
+import React from "react";
+import { mount } from "enzyme";
+import { checkQueueCycle, Uploader } from "../uploader";
+
+const testTimeout = 4000;
+
+describe("Uploader", () => {
+ test(
+ "Uploader will upload files in uploadQueue by interval",
+ () => {
+ // TODO: could be refactored using timer mocks
+ // https://facebook.github.io/jest/docs/en/timer-mocks.html
+ const tests = [
+ {
+ input: { target: { files: ["task1", "task2", "task3"] } },
+ uploadCalled: 3
+ }
+ ];
+
+ let promises = [];
+
+ const uploader = mount();
+ tests.forEach(testCase => {
+ // mock
+ const uploadSpy = jest.fn();
+ const uploadStub = () => {
+ uploadSpy();
+ return Promise.resolve();
+ };
+ uploader.instance().upload = uploadStub;
+ uploader.update();
+
+ // upload and verify
+ uploader.instance().onUpload(testCase.input);
+ const wait = testCase.input.target.files.length * 1000 + 100;
+ const promise = new Promise(resolve => {
+ setTimeout(() => {
+ expect(uploader.instance().state.uploadQueue.length).toBe(0);
+ expect(uploadSpy.mock.calls.length).toBe(testCase.uploadCalled);
+ resolve();
+ }, wait);
+ });
+
+ promises = [...promises, promise];
+ });
+
+ return Promise.all(promises);
+ },
+ testTimeout
+ );
+});
diff --git a/client/components/composite/time_grids.jsx b/client/components/composite/time_grids.jsx
new file mode 100644
index 0000000..7663ffb
--- /dev/null
+++ b/client/components/composite/time_grids.jsx
@@ -0,0 +1,69 @@
+import React from "react";
+import { config } from "../../config";
+import { Grids } from "../layout/grids";
+
+const styleTitle = {
+ color: "#fff",
+ backgroundColor: "rgba(0, 0, 0, 0.4)",
+ display: "inline-block",
+ padding: "0.5rem 1rem",
+ fontSize: "1rem",
+ margin: "2rem 0 0.5rem 0",
+ lineHeight: "1rem",
+ height: "1rem"
+};
+
+export class TimeGrids extends React.PureComponent {
+ render() {
+ const groups = new Map();
+
+ this.props.items.forEach(item => {
+ const date = new Date(item.timestamp);
+ const key = `${date.getFullYear()}-${date.getMonth() +
+ 1}-${date.getDate()}`;
+
+ if (groups.has(key)) {
+ groups.set(key, [...groups.get(key), item]);
+ } else {
+ groups.set(key, [item]);
+ }
+ });
+
+ var timeGrids = [];
+ groups.forEach((gridGroup, groupKey) => {
+ const year = parseInt(groupKey.split("-")[0]);
+ const month = parseInt(groupKey.split("-")[1]);
+ const date = parseInt(groupKey.split("-")[2]);
+
+ const sortedGroup = gridGroup.sort((item1, item2) => {
+ return item2.timestamp - item1.timestamp;
+ });
+
+ timeGrids = [
+ ...timeGrids,
+
+ ];
+ });
+
+ const sortedGroups = timeGrids.sort((group1, group2) => {
+ return group2.key - group1.key;
+ });
+ return {sortedGroups}
;
+ }
+}
+
+TimeGrids.defaultProps = {
+ items: [
+ {
+ key: "",
+ timestamp: -1,
+ component: no grid found
+ }
+ ],
+ styleContainer: {}
+};
diff --git a/client/components/composite/uploader.jsx b/client/components/composite/uploader.jsx
new file mode 100644
index 0000000..5e393ec
--- /dev/null
+++ b/client/components/composite/uploader.jsx
@@ -0,0 +1,215 @@
+import React from "react";
+import ReactDOM from "react-dom";
+
+import { config } from "../../config";
+import { Button } from "../control/button";
+import { getIcon } from "../display/icon";
+import { FileUploader } from "../../libs/api_upload";
+
+const msgFileNotFound = "File not found";
+const msgFileUploadOk = "is uploaded";
+const msgChromeLink = "https://www.google.com/chrome/";
+const msgFirefoxLink = "https://www.mozilla.org/";
+
+export const checkQueueCycle = 1000;
+
+const IconPlus = getIcon("cirUp");
+const IconThiList = getIcon("thList");
+
+const styleContainer = {
+ position: "fixed",
+ bottom: "0.5rem",
+ margin: "auto",
+ zIndex: 1
+};
+
+const styleButtonContainer = {
+ backgroundColor: "#2ecc71",
+ width: "20rem",
+ height: "auto",
+ textAlign: "center"
+};
+
+const styleDefault = {
+ color: "#fff"
+};
+
+const styleLabel = {
+ display: "inline-block",
+ verticalAlign: "middle",
+ marginLeft: "0.5rem"
+};
+
+const styleUploadQueue = {
+ backgroundColor: "#000",
+ opacity: 0.85,
+ color: "#fff",
+ fontSize: "0.75rem",
+ lineHeight: "1.25rem"
+};
+
+const styleUploadItem = {
+ width: "18rem",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ padding: "0.5rem 1rem"
+};
+
+const styleUnsupported = {
+ backgroundColor: "#e74c3c",
+ color: "#fff",
+ overflow: "hidden",
+ padding: "0.5rem 1rem",
+ width: "18rem",
+ textAlign: "center"
+};
+
+const styleStr = `
+ a {
+ color: white;
+ margin: auto 0.5rem auto 0.5rem;
+ }
+`;
+
+export class Uploader extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ uploadQueue: [],
+ uploadValue: ""
+ };
+
+ this.input = undefined;
+ this.assignInput = input => {
+ this.input = ReactDOM.findDOMNode(input);
+ };
+ }
+
+ componentDidMount() {
+ // will polling uploadQueue like a worker
+ this.checkQueue();
+ }
+
+ checkQueue = () => {
+ // TODO: using web worker to avoid lagging UI
+ if (this.state.uploadQueue.length > 0) {
+ this.upload(this.state.uploadQueue[0]).then(() => {
+ this.setState({ uploadQueue: this.state.uploadQueue.slice(1) });
+ setTimeout(this.checkQueue, checkQueueCycle);
+ });
+ } else {
+ setTimeout(this.checkQueue, checkQueueCycle);
+ }
+ };
+
+ upload = file => {
+ const fileUploader = new FileUploader(
+ this.onStart,
+ this.onProgress,
+ this.onFinish,
+ this.onError
+ );
+
+ return fileUploader.uploadFile(file);
+ };
+
+ onStart = () => {
+ this.props.onRefresh();
+ };
+
+ onProgress = (shareId, progress) => {
+ this.props.onUpdateProgress(shareId, progress);
+ };
+
+ onFinish = () => {
+ this.props.onRefresh();
+ };
+
+ onError = err => {
+ this.props.onError(err);
+ };
+
+ onUpload = event => {
+ if (event.target.files == null || event.target.files.length === 0) {
+ this.props.onError(msgFileNotFound);
+ this.setState({ uploadValue: "" });
+ } else {
+ this.setState({
+ uploadQueue: [...this.state.uploadQueue, ...event.target.files],
+ uploadValue: ""
+ });
+ }
+ };
+
+ onChooseFile = () => {
+ this.input.click();
+ };
+
+ render() {
+ if (
+ window.FormData == null ||
+ window.FileReader == null ||
+ window.Blob == null
+ ) {
+ return (
+
+ );
+ }
+
+ const hiddenInput = (
+
+ );
+
+ const uploadQueue = this.state.uploadQueue.map(file => {
+ return (
+
+
+ {file.name}
+
+ );
+ });
+
+ return (
+
+
{uploadQueue}
+
}
+ styleDefault={styleDefault}
+ styleContainer={styleButtonContainer}
+ styleLabel={styleLabel}
+ />
+ {hiddenInput}
+
+ );
+ }
+}
+
+Uploader.defaultProps = {
+ onRefresh: () => console.error("undefined"),
+ onUpdateProgress: () => console.error("undefined"),
+ onOk: () => console.error("undefined"),
+ onError: () => console.error("undefined")
+};
diff --git a/client/components/control/button.jsx b/client/components/control/button.jsx
new file mode 100644
index 0000000..8318669
--- /dev/null
+++ b/client/components/control/button.jsx
@@ -0,0 +1,102 @@
+import React from "react";
+
+const buttonClassName = "btn";
+
+const styleContainer = {
+ display: "inline-block",
+ height: "2.5rem"
+};
+
+const styleIcon = {
+ lineHeight: "2.5rem",
+ height: "2.5rem",
+ margin: "0 -0.25rem 0 0.5rem"
+};
+
+const styleBase = {
+ background: "transparent",
+ lineHeight: "2.5rem",
+ fontSize: "0.875rem",
+ border: "none",
+ outline: "none",
+ padding: "0 0.75rem",
+ textAlign: "center"
+};
+
+const styleDefault = {
+ ...styleBase
+};
+
+const styleStr = `
+ .${buttonClassName}:hover {
+ opacity: 0.7;
+ transition: opacity 0.25s;
+ }
+
+ .${buttonClassName}:active {
+ opacity: 0.7;
+ transition: opacity 0.25s;
+ }
+
+ .${buttonClassName}:disabled {
+ opacity: 0.2;
+ transition: opacity 0.25s;
+ }
+
+ button::-moz-focus-inner {
+ border: 0;
+ }
+`;
+
+export class Button extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.styleDefault = { ...styleDefault, ...this.props.styleDefault };
+ this.styleStr = this.props.styleStr ? this.props.styleStr : styleStr;
+ }
+
+ onClick = e => {
+ if (this.props.onClick && this.props.isEnabled) {
+ this.props.onClick(e);
+ }
+ };
+
+ render() {
+ const style = this.props.isEnabled ? this.styleDefault : this.styleDisabled;
+ const icon =
+ this.props.icon != null ? (
+
+ {this.props.icon}
+
+ ) : (
+
+ );
+
+ return (
+
+ {icon}
+
+
+ );
+ }
+}
+
+Button.defaultProps = {
+ className: "btn",
+ isEnabled: true,
+ icon: null,
+ onClick: () => true,
+ styleContainer: {},
+ styleDefault: {},
+ styleDisabled: {},
+ styleLabel: {},
+ styleIcon: {},
+ styleStr: undefined
+};
diff --git a/client/components/control/input.jsx b/client/components/control/input.jsx
new file mode 100644
index 0000000..f69e38d
--- /dev/null
+++ b/client/components/control/input.jsx
@@ -0,0 +1,128 @@
+import React from "react";
+
+const styleContainer = {
+ backgroundColor: "#ccc",
+ display: "inline-block",
+ height: "2.5rem"
+};
+
+const styleIcon = {
+ lineHeight: "2.5rem",
+ height: "2.5rem",
+ margin: "0 0.25rem 0 0.5rem"
+};
+
+const styleInputBase = {
+ backgroundColor: "transparent",
+ border: "none",
+ display: "inline-block",
+ fontSize: "0.875rem",
+ height: "2.5rem",
+ lineHeight: "2.5rem",
+ outline: "none",
+ overflowY: "hidden",
+ padding: "0 0.75rem",
+ verticalAlign: "middle"
+};
+
+const styleDefault = {
+ ...styleInputBase,
+ color: "#333"
+};
+
+const styleInvalid = {
+ ...styleInputBase,
+ color: "#e74c3c"
+};
+
+const inputClassName = "qs-input";
+const styleStr = `
+.${inputClassName}:hover {
+ // box-shadow: 0px 0px -5px rgba(0, 0, 0, 1);
+ opacity: 0.7;
+ transition: opacity 0.25s;
+}
+
+.${inputClassName}:active {
+ // box-shadow: 0px 0px -5px rgba(0, 0, 0, 1);
+ opacity: 0.7;
+ transition: opacity 0.25s;
+}
+
+.${inputClassName}:disabled {
+ color: #ccc;
+}
+`;
+
+export class Input extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = { isValid: true };
+ this.inputRef = undefined;
+ }
+
+ onChange = e => {
+ this.props.onChange(e.target.value);
+ this.props.onChangeEvent(e);
+ this.props.onChangeTarget(e.target);
+ this.setState({ isValid: this.props.validate(e.target.value) });
+ };
+
+ getRef = input => {
+ this.inputRef = input;
+ this.props.inputRef(this.inputRef);
+ };
+
+ render() {
+ const style = this.state.isValid ? styleDefault : styleInvalid;
+ const icon =
+ this.props.icon != null ? (
+ {this.props.icon}
+ ) : (
+
+ );
+
+ return (
+
+ {icon}
+
+
+
+ );
+ }
+}
+
+Input.defaultProps = {
+ className: "input",
+ maxLength: "32",
+ placeholder: "placeholder",
+ readOnly: false,
+ style: {},
+ styleContainer: {},
+ styleInvalid: {},
+ type: "text",
+ disabled: false,
+ width: "auto",
+ value: "",
+ icon: null,
+ onChange: () => true,
+ onChangeEvent: () => true,
+ onChangeTarget: () => true,
+ validate: () => true,
+ inputRef: () => true
+};
diff --git a/client/components/display/icon.js b/client/components/display/icon.js
new file mode 100644
index 0000000..2190eb0
--- /dev/null
+++ b/client/components/display/icon.js
@@ -0,0 +1,177 @@
+const IconFile = require("react-icons/lib/fa/file-o");
+const IconImg = require("react-icons/lib/md/image");
+const IconZip = require("react-icons/lib/md/archive");
+const IconVideo = require("react-icons/lib/md/ondemand-video");
+const IconAudio = require("react-icons/lib/md/music-video");
+const IconText = require("react-icons/lib/md/description");
+const IconExcel = require("react-icons/lib/fa/file-excel-o");
+const IconPPT = require("react-icons/lib/fa/file-powerpoint-o");
+const IconPdf = require("react-icons/lib/md/picture-as-pdf");
+const IconWord = require("react-icons/lib/fa/file-word-o");
+const IconCode = require("react-icons/lib/md/code");
+const IconApk = require("react-icons/lib/md/android");
+const IconExe = require("react-icons/lib/fa/cog");
+
+const IconBars = require("react-icons/lib/fa/bars");
+const IconSpinner = require("react-icons/lib/md/autorenew");
+const IconCirUp = require("react-icons/lib/fa/arrow-circle-up");
+const IconSignIn = require("react-icons/lib/fa/sign-in");
+const IconSignOut = require("react-icons/lib/fa/sign-out");
+const IconAngUp = require("react-icons/lib/fa/angle-up");
+const IconAngRight = require("react-icons/lib/fa/angle-right");
+const IconAngDown = require("react-icons/lib/fa/angle-down");
+const IconAngLeft = require("react-icons/lib/fa/angle-left");
+const IconTimesCir = require("react-icons/lib/md/cancel");
+const IconPlusSqu = require("react-icons/lib/md/add-box");
+const IconPlusCir = require("react-icons/lib/fa/plus-circle");
+const IconPlus = require("react-icons/lib/md/add");
+const IconSearch = require("react-icons/lib/fa/search");
+const IconThList = require("react-icons/lib/fa/th-list");
+const IconCalendar = require("react-icons/lib/fa/calendar-o");
+
+const IconCheckCir = require("react-icons/lib/fa/check-circle");
+const IconExTri = require("react-icons/lib/fa/exclamation-triangle");
+const IconInfoCir = require("react-icons/lib/fa/info-circle");
+const IconRefresh = require("react-icons/lib/fa/refresh");
+
+const fileTypeIconMap = {
+ // text
+ txt: { icon: IconText, color: "#333" },
+ rtf: { icon: IconText, color: "#333" },
+ htm: { icon: IconText, color: "#333" },
+ html: { icon: IconText, color: "#333" },
+ xml: { icon: IconText, color: "#333" },
+ yml: { icon: IconText, color: "#333" },
+ json: { icon: IconText, color: "#333" },
+ toml: { icon: IconText, color: "#333" },
+ md: { icon: IconText, color: "#333" },
+ // office
+ ppt: { icon: IconPPT, color: "#e67e22" },
+ pptx: { icon: IconPPT, color: "#e67e22" },
+ xls: { icon: IconExcel, color: "#16a085" },
+ xlsx: { icon: IconExcel, color: "#16a085" },
+ xlsm: { icon: IconExcel, color: "#16a085" },
+ doc: { icon: IconWord, color: "#2980b9" },
+ docx: { icon: IconWord, color: "#2980b9" },
+ docx: { icon: IconWord, color: "#2980b9" },
+ pdf: { icon: IconPdf, color: "#c0392b" },
+ // code
+ c: { icon: IconCode, color: "#666" },
+ cpp: { icon: IconCode, color: "#666" },
+ java: { icon: IconCode, color: "#666" },
+ js: { icon: IconCode, color: "#666" },
+ py: { icon: IconCode, color: "#666" },
+ pyc: { icon: IconCode, color: "#666" },
+ rb: { icon: IconCode, color: "#666" },
+ php: { icon: IconCode, color: "#666" },
+ go: { icon: IconCode, color: "#666" },
+ sh: { icon: IconCode, color: "#666" },
+ vb: { icon: IconCode, color: "#666" },
+ sql: { icon: IconCode, color: "#666" },
+ r: { icon: IconCode, color: "#666" },
+ swift: { icon: IconCode, color: "#666" },
+ oc: { icon: IconCode, color: "#666" },
+ // misc
+ apk: { icon: IconApk, color: "#2ecc71" },
+ exe: { icon: IconExe, color: "#333" },
+ deb: { icon: IconExe, color: "#333" },
+ rpm: { icon: IconExe, color: "#333" },
+ // img
+ bmp: { icon: IconImg, color: "#1abc9c" },
+ gif: { icon: IconImg, color: "#1abc9c" },
+ jpg: { icon: IconImg, color: "#1abc9c" },
+ jpeg: { icon: IconImg, color: "#1abc9c" },
+ tiff: { icon: IconImg, color: "#1abc9c" },
+ psd: { icon: IconImg, color: "#1abc9c" },
+ png: { icon: IconImg, color: "#1abc9c" },
+ svg: { icon: IconImg, color: "#1abc9c" },
+ pcx: { icon: IconImg, color: "#1abc9c" },
+ dxf: { icon: IconImg, color: "#1abc9c" },
+ wmf: { icon: IconImg, color: "#1abc9c" },
+ emf: { icon: IconImg, color: "#1abc9c" },
+ eps: { icon: IconImg, color: "#1abc9c" },
+ tga: { icon: IconImg, color: "#1abc9c" },
+ // compress
+ gz: { icon: IconZip, color: "#34495e" },
+ zip: { icon: IconZip, color: "#34495e" },
+ "7z": { icon: IconZip, color: "#34495e" },
+ rar: { icon: IconZip, color: "#34495e" },
+ tar: { icon: IconZip, color: "#34495e" },
+ gzip: { icon: IconZip, color: "#34495e" },
+ cab: { icon: IconZip, color: "#34495e" },
+ uue: { icon: IconZip, color: "#34495e" },
+ arj: { icon: IconZip, color: "#34495e" },
+ bz2: { icon: IconZip, color: "#34495e" },
+ lzh: { icon: IconZip, color: "#34495e" },
+ jar: { icon: IconZip, color: "#34495e" },
+ ace: { icon: IconZip, color: "#34495e" },
+ iso: { icon: IconZip, color: "#34495e" },
+ z: { icon: IconZip, color: "#34495e" },
+ // video
+ asf: { icon: IconVideo, color: "#f39c12" },
+ avi: { icon: IconVideo, color: "#f39c12" },
+ flv: { icon: IconVideo, color: "#f39c12" },
+ mkv: { icon: IconVideo, color: "#f39c12" },
+ mov: { icon: IconVideo, color: "#f39c12" },
+ mp4: { icon: IconVideo, color: "#f39c12" },
+ mpeg: { icon: IconVideo, color: "#f39c12" },
+ mpg: { icon: IconVideo, color: "#f39c12" },
+ ram: { icon: IconVideo, color: "#f39c12" },
+ rmvb: { icon: IconVideo, color: "#f39c12" },
+ qt: { icon: IconVideo, color: "#f39c12" },
+ wmv: { icon: IconVideo, color: "#f39c12" },
+ // audio
+ cda: { icon: IconAudio, color: "#d35400" },
+ cmf: { icon: IconAudio, color: "#d35400" },
+ mid: { icon: IconAudio, color: "#d35400" },
+ mp1: { icon: IconAudio, color: "#d35400" },
+ mp2: { icon: IconAudio, color: "#d35400" },
+ mp3: { icon: IconAudio, color: "#d35400" },
+ rm: { icon: IconAudio, color: "#d35400" },
+ rmi: { icon: IconAudio, color: "#d35400" },
+ vqf: { icon: IconAudio, color: "#d35400" },
+ wav: { icon: IconAudio, color: "#d35400" }
+};
+
+const fileIconMap = {
+ ...fileTypeIconMap,
+ // other
+ spinner: { icon: IconSpinner, color: "#1abc9c" },
+ cirup: { icon: IconCirUp, color: "#fff" },
+ signin: { icon: IconSignIn, color: "#fff" },
+ signout: { icon: IconSignOut, color: "#fff" },
+ angup: { icon: IconAngUp, color: "#2c3e50" },
+ angright: { icon: IconAngRight, color: "#2c3e50" },
+ angdown: { icon: IconAngDown, color: "#2c3e50" },
+ angleft: { icon: IconAngLeft, color: "#2c3e50" },
+ timescir: { icon: IconTimesCir, color: "#c0392b" },
+ plussqu: { icon: IconPlusSqu, color: "#2ecc71" },
+ pluscir: { icon: IconPlusCir, color: "#2ecc71" },
+ plus: { icon: IconPlus, color: "#2ecc71" },
+ search: { icon: IconSearch, color: "#ccc" },
+ checkcir: { icon: IconCheckCir, color: "#27ae60" },
+ extri: { icon: IconExTri, color: "#f39c12" },
+ infocir: { icon: IconInfoCir, color: "#2c3e50" },
+ refresh: { icon: IconRefresh, color: "#8e44ad" },
+ thlist: { icon: IconThList, color: "#fff" },
+ bars: { icon: IconBars, color: "#666" },
+ calendar: { icon: IconCalendar, color: "#333" }
+};
+
+export const getIcon = extend => {
+ if (fileIconMap[extend.toUpperCase()]) {
+ return fileIconMap[extend.toUpperCase()].icon;
+ } else if (fileIconMap[extend.toLowerCase()]) {
+ return fileIconMap[extend.toLowerCase()].icon;
+ }
+ return IconFile;
+};
+
+export const getIconColor = extend => {
+ if (fileIconMap[extend.toUpperCase()]) {
+ return fileIconMap[extend.toUpperCase()].color;
+ } else if (fileIconMap[extend.toLowerCase()]) {
+ return fileIconMap[extend.toLowerCase()].color;
+ }
+ return "#333";
+};
diff --git a/client/components/layout/grids.jsx b/client/components/layout/grids.jsx
new file mode 100644
index 0000000..d99ecdb
--- /dev/null
+++ b/client/components/layout/grids.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+
+const styleGridBase = {
+ float: "left",
+ margin: 0
+};
+
+export const Grids = props => (
+
+ {props.nodes.map(node => (
+
+ {node.component}
+
+ ))}
+
+
+);
+
+Grids.defaultProps = {
+ nodes: [{ key: "key", component: , style: {} }],
+ gridStyle: styleGridBase,
+ containerStyle: {}
+};
diff --git a/client/config.js b/client/config.js
new file mode 100644
index 0000000..e04b9a3
--- /dev/null
+++ b/client/config.js
@@ -0,0 +1,7 @@
+export const config = {
+ serverAddr: "",
+ testId: "admin",
+ testPwd: "quicksh@re",
+ rootSize: 16,
+ colWidth: 20
+};
diff --git a/client/libs/__mocks__/api_auth.js b/client/libs/__mocks__/api_auth.js
new file mode 100644
index 0000000..7c5f3aa
--- /dev/null
+++ b/client/libs/__mocks__/api_auth.js
@@ -0,0 +1,7 @@
+export function login(serverAddr, adminId, adminPwd, axiosConfig) {
+ return Promise.resolve(true);
+}
+
+export function logout(serverAddr, axiosConfig) {
+ return Promise.resolve(true);
+}
diff --git a/client/libs/__mocks__/api_share.js b/client/libs/__mocks__/api_share.js
new file mode 100644
index 0000000..98db7d8
--- /dev/null
+++ b/client/libs/__mocks__/api_share.js
@@ -0,0 +1,41 @@
+let _infos = [];
+const shadowedId = "shadowedId";
+const publicId = "publicId";
+
+export function __addInfos(infos) {
+ _infos = [..._infos, ...infos];
+}
+
+export function __truncInfos(info) {
+ _infos = [];
+}
+
+export const del = shareId => {
+ _infos = _infos.filter(info => {
+ return !info.shareId == shareId;
+ });
+ return Promise.resolve(true);
+};
+
+export const list = () => {
+ return Promise.resolve(_infos);
+};
+
+export const shadowId = shareId => {
+ return Promise.resolve(shadowedId);
+};
+
+export const publishId = shareId => {
+ return Promise.resolve(publicId);
+};
+
+export const setDownLimit = (shareId, downLimit) => {
+ _infos = _infos.map(info => {
+ return info.shareId == shareId ? { ...info, downLimit } : info;
+ });
+ return Promise.resolve(true);
+};
+
+export const addLocalFiles = () => {
+ return Promise.resolve(true);
+};
diff --git a/client/libs/api_auth.js b/client/libs/api_auth.js
new file mode 100644
index 0000000..eee72bc
--- /dev/null
+++ b/client/libs/api_auth.js
@@ -0,0 +1,29 @@
+import axios from "axios";
+import { config } from "../config";
+import { makePostBody } from "./utils";
+
+export function login(serverAddr, adminId, adminPwd, axiosConfig) {
+ return axios
+ .post(
+ `${serverAddr}/login`,
+ makePostBody(
+ {
+ act: "login",
+ adminid: adminId,
+ adminpwd: adminPwd
+ },
+ axiosConfig
+ )
+ )
+ .then(response => {
+ return response.data.Code === 200;
+ });
+}
+
+export function logout(serverAddr, axiosConfig) {
+ return axios
+ .post(`${serverAddr}/login`, makePostBody({ act: "logout" }), axiosConfig)
+ .then(response => {
+ return response.data.Code === 200;
+ });
+}
diff --git a/client/libs/api_share.js b/client/libs/api_share.js
new file mode 100644
index 0000000..88164c4
--- /dev/null
+++ b/client/libs/api_share.js
@@ -0,0 +1,51 @@
+import axios from "axios";
+import { config } from "../config";
+
+export const del = shareId => {
+ return axios
+ .delete(`${config.serverAddr}/fileinfo?shareid=${shareId}`)
+ .then(response => response.data.Code === 200);
+};
+
+export const list = () => {
+ return axios.get(`${config.serverAddr}/fileinfo`).then(response => {
+ // TODO check status code
+ return response.data.List;
+ });
+};
+
+export const shadowId = shareId => {
+ const act = "shadowid";
+ return axios
+ .patch(`${config.serverAddr}/fileinfo?act=${act}&shareid=${shareId}`)
+ .then(response => {
+ return response.data.ShareId;
+ });
+};
+
+export const publishId = shareId => {
+ const act = "publishid";
+ return axios
+ .patch(`${config.serverAddr}/fileinfo?act=${act}&shareid=${shareId}`)
+ .then(response => {
+ return response.data.ShareId;
+ });
+};
+
+export const setDownLimit = (shareId, downLimit) => {
+ const act = "setdownlimit";
+ return axios
+ .patch(
+ `${
+ config.serverAddr
+ }/fileinfo?act=${act}&shareid=${shareId}&downlimit=${downLimit}`
+ )
+ .then(response => response.data.Code === 200);
+};
+
+export const addLocalFiles = () => {
+ const act = "addlocalfiles";
+ return axios
+ .patch(`${config.serverAddr}/fileinfo?act=${act}`)
+ .then(response => response.data.Code === 200);
+};
diff --git a/client/libs/api_upload.js b/client/libs/api_upload.js
new file mode 100644
index 0000000..d8f122e
--- /dev/null
+++ b/client/libs/api_upload.js
@@ -0,0 +1,202 @@
+import axios from "axios";
+import { config } from "../config";
+import { makePostBody } from "./utils";
+
+const wait = 5000; // TODO: should tune according to backend
+const retryMax = 100000;
+const maxUploadLen = 20 * 1024 * 1024;
+
+// TODO: add to react-intl
+const msgUploadFailed = "Fail to upload, upload is stopped.";
+const msgUploadFailedAndRetry = "Fail to upload, retrying...";
+const msgFileExists = "File exists.";
+const msgTooBigChunk = "Too big chunk.";
+const msgFileNotFound = "File not found, upload stopped.";
+
+function randomWait() {
+ return Math.random() * wait;
+}
+
+function isKnownErr(res) {
+ return res != null && res.Code != null && res.Msg != null;
+}
+
+export class FileUploader {
+ constructor(onStart, onProgress, onFinish, onError) {
+ this.onStart = onStart;
+ this.onProgress = onProgress;
+ this.onFinish = onFinish;
+ this.onError = onError;
+ this.retry = retryMax;
+ this.reader = new FileReader();
+
+ this.uploadFile = file => {
+ return this.startUpload(file);
+ };
+
+ this.startUpload = file => {
+ return axios
+ .post(
+ `${config.serverAddr}/startupload`,
+ makePostBody({
+ fname: file.name
+ })
+ )
+ .then(response => {
+ if (
+ response.data.ShareId == null ||
+ response.data.Start === null ||
+ response.data.Length === null
+ ) {
+ throw response;
+ } else {
+ this.onStart(response.data.ShareId, file.name);
+ return this.upload(
+ {
+ shareId: response.data.ShareId,
+ start: response.data.Start,
+ length: response.data.Length
+ },
+ file
+ );
+ }
+ })
+ .catch(response => {
+ // TODO: this is not good because error may not be response
+ if (isKnownErr(response.data) && response.data.Code === 429) {
+ setTimeout(this.startUpload, randomWait(), file);
+ } else if (isKnownErr(response.data) && response.data.Code === 412) {
+ this.onError(msgFileExists);
+ } else if (isKnownErr(response.data) && response.data.Code === 404) {
+ this.onError(msgFileNotFound);
+ } else if (this.retry > 0) {
+ this.retry--;
+ this.onError(msgUploadFailedAndRetry);
+ console.trace(response);
+ setTimeout(this.startUpload, randomWait(), file);
+ } else {
+ this.onError(msgUploadFailed);
+ console.trace(response);
+ }
+ });
+ };
+
+ this.prepareReader = (shareInfo, end, resolve, reject) => {
+ this.reader.onerror = err => {
+ reject(err);
+ };
+
+ this.reader.onloadend = event => {
+ const formData = new FormData();
+ formData.append("shareid", shareInfo.shareId);
+ formData.append("start", shareInfo.start);
+ formData.append("len", end - shareInfo.start);
+ formData.append("chunk", new Blob([event.target.result]));
+
+ const url = `${config.serverAddr}/upload`;
+ const headers = {
+ "Content-Type": "multipart/form-data"
+ };
+
+ try {
+ axios
+ .post(url, formData, { headers })
+ .then(response => resolve(response))
+ .catch(err => {
+ reject(err);
+ });
+ } catch (err) {
+ reject(err);
+ }
+ };
+ };
+
+ this.upload = (shareInfo, file) => {
+ const uploaded = shareInfo.start + shareInfo.length;
+ const end = uploaded < file.size ? uploaded : file.size;
+
+ return new Promise((resolve, reject) => {
+ if (
+ end == null ||
+ shareInfo.start == null ||
+ end - shareInfo.start >= maxUploadLen
+ ) {
+ throw new Error(msgTooBigChunk);
+ }
+
+ const chunk = file.slice(shareInfo.start, end);
+ this.prepareReader(shareInfo, end, resolve, reject);
+ this.reader.readAsArrayBuffer(chunk);
+ })
+ .then(response => {
+ if (
+ response.data.ShareId == null ||
+ response.data.Start == null ||
+ response.data.Length == null ||
+ response.data.Start !== end
+ ) {
+ throw response;
+ } else {
+ if (end < file.size) {
+ this.onProgress(shareInfo.shareId, end / file.size);
+ return this.upload(
+ {
+ shareId: shareInfo.shareId,
+ start: shareInfo.start + shareInfo.length,
+ length: shareInfo.length
+ },
+ file
+ );
+ } else {
+ return this.finishUpload(shareInfo);
+ }
+ }
+ })
+ .catch(response => {
+ // possible error: response.data.Start == null || response.data.Start !== end
+ if (isKnownErr(response.data) && response.data.Code === 429) {
+ setTimeout(this.upload, randomWait(), shareInfo, file);
+ } else if (isKnownErr(response.data) && response.data.Code === 404) {
+ this.onError(msgFileNotFound);
+ } else if (this.retry > 0) {
+ this.retry--;
+ setTimeout(this.upload, randomWait(), shareInfo, file);
+ this.onError(msgUploadFailedAndRetry);
+ console.trace(response);
+ } else {
+ this.onError(msgUploadFailed);
+ console.trace(response);
+ }
+ });
+ };
+
+ this.finishUpload = shareInfo => {
+ return axios
+ .post(`${config.serverAddr}/finishupload?shareid=${shareInfo.shareId}`)
+ .then(response => {
+ // TODO: should check Code instead of Url
+ if (response.data.ShareId != null && response.data.Start == null) {
+ this.onFinish();
+ return response.data.ShareId;
+ } else {
+ throw response;
+ }
+ })
+ .catch(response => {
+ if (isKnownErr(response.data) && response.data.Code === 429) {
+ setTimeout(this.finishUpload, randomWait(), shareInfo);
+ } else if (isKnownErr(response.data) && response.data.Code === 404) {
+ this.onError(msgFileNotFound);
+ } else if (this.retry > 0) {
+ this.retry--;
+ setTimeout(this.finishUpload, randomWait(), shareInfo);
+ this.onError(msgUploadFailedAndRetry);
+ console.trace(response);
+ } else {
+ this.onError(msgUploadFailed);
+ console.trace(response);
+ }
+ });
+ };
+ }
+}
diff --git a/client/libs/file_type.js b/client/libs/file_type.js
new file mode 100644
index 0000000..0e89473
--- /dev/null
+++ b/client/libs/file_type.js
@@ -0,0 +1,18 @@
+const fileTypeMap = {
+ jpg: "image",
+ jpeg: "image",
+ png: "image",
+ bmp: "image",
+ gz: "archive",
+ mov: "video",
+ mp4: "video",
+ mov: "video",
+ avi: "video"
+};
+
+export const getFileExt = fileName => fileName.split(".").pop();
+
+export const getFileType = fileName => {
+ const ext = getFileExt(fileName);
+ return fileTypeMap[ext] != null ? fileTypeMap[ext] : "file";
+};
diff --git a/client/libs/test/api_auth_test.js b/client/libs/test/api_auth_test.js
new file mode 100644
index 0000000..29cd955
--- /dev/null
+++ b/client/libs/test/api_auth_test.js
@@ -0,0 +1,34 @@
+import { login, logout } from "../api_auth";
+import { config } from "../../config";
+
+const serverAddr = config.serverAddr;
+const testId = config.testId;
+const testPwd = config.testPwd;
+
+export function testAuth() {
+ return testLogin()
+ .then(testLogout)
+ .catch(err => {
+ console.error("auth: fail", err);
+ });
+}
+
+export function testLogin() {
+ return login(serverAddr, testId, testPwd).then(ok => {
+ if (ok === true) {
+ console.log("login api: ok");
+ } else {
+ throw new Error("login api: failed");
+ }
+ });
+}
+
+export function testLogout() {
+ return logout(serverAddr).then(ok => {
+ if (ok === true) {
+ console.log("logout api: ok");
+ } else {
+ throw new Error("logout api: failed");
+ }
+ });
+}
diff --git a/client/libs/test/api_share_test.js b/client/libs/test/api_share_test.js
new file mode 100644
index 0000000..99ad2a1
--- /dev/null
+++ b/client/libs/test/api_share_test.js
@@ -0,0 +1,167 @@
+import { FileUploader } from "../api_upload";
+import {
+ del,
+ list,
+ shadowId,
+ publishId,
+ setDownLimit,
+ addLocalFiles
+} from "../api_share";
+import { testLogin, testLogout } from "./api_auth_test";
+
+const fileName = "filename";
+
+function upload(fileName) {
+ return new Promise(resolve => {
+ const onStart = () => true;
+ const onProgress = () => true;
+ const onFinish = () => resolve();
+ const onError = err => {
+ throw new Error(JSON.stringify(err));
+ };
+ const file = new File(["foo"], fileName, {
+ type: "text/plain"
+ });
+
+ const uploader = new FileUploader(onStart, onProgress, onFinish, onError);
+ uploader.uploadFile(file);
+ });
+}
+
+function getIdFromList(list, fileName) {
+ if (list == null) {
+ throw new Error("list: list fail");
+ }
+
+ // TODO: should verify file name
+ const filterInfo = list.find(info => {
+ return info.PathLocal.includes(fileName);
+ });
+
+ if (filterInfo == null) {
+ console.error(list);
+ throw new Error("list: file name not found");
+ } else {
+ return filterInfo.Id;
+ }
+}
+
+function delWithName(fileName) {
+ return list().then(infoList => {
+ const infoToDel = infoList.find(info => {
+ return info.PathLocal.includes(fileName);
+ });
+
+ if (infoToDel == null) {
+ console.warn("delWithName: name not found");
+ } else {
+ return del(infoToDel.Id);
+ }
+ });
+}
+
+export function testShadowPublishId() {
+ return testLogin()
+ .then(() => upload(fileName))
+ .then(list)
+ .then(infoList => {
+ return getIdFromList(infoList, fileName);
+ })
+ .then(shareId => {
+ return shadowId(shareId).then(secretId => {
+ if (shareId === secretId) {
+ throw new Error("shadowId: id not changed");
+ } else {
+ return secretId;
+ }
+ });
+ })
+ .then(secretId => {
+ return list().then(infoList => {
+ const info = infoList.find(info => {
+ return info.Id === secretId;
+ });
+
+ if (info.PathLocal.includes(fileName)) {
+ console.log("shadowId api: ok", secretId);
+ return secretId;
+ } else {
+ throw new Error("shadowId pai: file not found", infoList, fileName);
+ }
+ });
+ })
+ .then(secretId => {
+ return publishId(secretId).then(publicId => {
+ if (publicId === secretId) {
+ // TODO: it is not enough to check they are not equal
+ throw new Error("publicId: id not changed");
+ } else {
+ console.log("publishId api: ok", publicId);
+ return publicId;
+ }
+ });
+ })
+ .then(shareId => del(shareId))
+ .then(testLogout)
+ .catch(err => {
+ console.error(err);
+ delWithName(fileName);
+ });
+}
+
+export function testSetDownLimit() {
+ const downLimit = 777;
+
+ return testLogin()
+ .then(() => upload(fileName))
+ .then(list)
+ .then(infoList => {
+ return getIdFromList(infoList, fileName);
+ })
+ .then(shareId => {
+ return setDownLimit(shareId, downLimit).then(ok => {
+ if (!ok) {
+ throw new Error("setDownLimit: failed");
+ } else {
+ return shareId;
+ }
+ });
+ })
+ .then(shareId => {
+ return list().then(infoList => {
+ const info = infoList.find(info => {
+ return info.Id == shareId;
+ });
+
+ if (info.DownLimit === downLimit) {
+ console.log("setDownLimit api: ok");
+ return shareId;
+ } else {
+ throw new Error("setDownLimit api: limit unchanged");
+ }
+ });
+ })
+ .then(shareId => del(shareId))
+ .then(testLogout)
+ .catch(err => {
+ console.error(err);
+ delWithName(fileName);
+ });
+}
+
+// TODO: need to add local file and test
+export function testAddLocalFiles() {
+ return testLogin()
+ .then(() => addLocalFiles())
+ .then(ok => {
+ if (ok) {
+ console.log("addLocalFiles api: ok");
+ } else {
+ throw new Error("addLocalFiles api: failed");
+ }
+ })
+ .then(() => testLogout())
+ .catch(err => {
+ console.error(err);
+ });
+}
diff --git a/client/libs/test/api_test.js b/client/libs/test/api_test.js
new file mode 100644
index 0000000..5ff75cf
--- /dev/null
+++ b/client/libs/test/api_test.js
@@ -0,0 +1,25 @@
+import { testAuth } from "./api_auth_test";
+import { testUploadOneFile } from "./api_upload_test";
+import {
+ testAddLocalFiles,
+ testSetDownLimit,
+ testShadowPublishId
+} from "./api_share_test";
+import { testUpDownBatch } from "./api_up_down_batch_test";
+
+console.log("Test started");
+
+const fileName = `test_filename${Date.now()}`;
+const file = new File(["foo"], fileName, {
+ type: "text/plain"
+});
+
+testAuth()
+ .then(testShadowPublishId)
+ .then(() => testUploadOneFile(file, fileName))
+ .then(testSetDownLimit)
+ .then(testAddLocalFiles)
+ .then(testUpDownBatch)
+ .then(() => {
+ console.log("Tests are finished");
+ });
diff --git a/client/libs/test/api_up_down_batch_test.js b/client/libs/test/api_up_down_batch_test.js
new file mode 100644
index 0000000..6011b1d
--- /dev/null
+++ b/client/libs/test/api_up_down_batch_test.js
@@ -0,0 +1,97 @@
+import axios from "axios";
+import md5 from "md5";
+
+import { config } from "../../config";
+import { testUpload } from "./api_upload_test";
+import { list, del } from "../api_share";
+import { testLogin, testLogout } from "./api_auth_test";
+
+export function testUpDownBatch() {
+ const fileInfos = [
+ {
+ fileName: "test_2MB_1",
+ content: new Array(1024 * 1024 * 2).join("x")
+ },
+ {
+ fileName: "test_1MB_1",
+ content: new Array(1024 * 1024 * 1).join("x")
+ },
+ {
+ fileName: "test_2MB_2",
+ content: new Array(1024 * 1024 * 2).join("x")
+ },
+ {
+ fileName: "test_1B",
+ content: `${new Array(3).join("o")}${new Array(3).join("x")}`
+ }
+ ];
+
+ return testLogin()
+ .then(() => {
+ const promises = fileInfos.map(info => {
+ const file = new File([info.content], info.fileName, {
+ type: "text/plain"
+ });
+
+ return testUpAndDownOneFile(file, info.fileName);
+ });
+
+ return Promise.all(promises);
+ })
+ .then(() => {
+ testLogout();
+ })
+ .catch(err => console.error(err));
+}
+
+export function testUpAndDownOneFile(file, fileName) {
+ return delTestFile(fileName)
+ .then(() => testUpload(file))
+ .then(shareId => testDownload(shareId, file))
+ .catch(err => console.error(err));
+}
+
+function delTestFile(fileName) {
+ return list().then(infos => {
+ const info = infos.find(info => {
+ return info.PathLocal === fileName;
+ });
+
+ if (info == null) {
+ console.log("up-down: file not found", fileName);
+ } else {
+ return del(info.Id);
+ }
+ });
+}
+
+function testDownload(shareId, file) {
+ return axios
+ .get(`${config.serverAddr}/download?shareid=${shareId}`)
+ .then(response => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = event => {
+ const upHash = md5(event.target.result);
+ const downHash = md5(response.data);
+ if (upHash !== downHash) {
+ console.error(
+ "up&down: hash unmatch",
+ file.name,
+ upHash,
+ downHash,
+ upHash.length,
+ downHash.length
+ );
+ } else {
+ console.log("up&down: ok: hash match", file.name, upHash, downHash);
+ resolve();
+ }
+ };
+
+ reader.onerror = err => reject(err);
+
+ reader.readAsText(file);
+ });
+ });
+}
diff --git a/client/libs/test/api_upload_test.js b/client/libs/test/api_upload_test.js
new file mode 100644
index 0000000..bbd3d99
--- /dev/null
+++ b/client/libs/test/api_upload_test.js
@@ -0,0 +1,63 @@
+import { FileUploader } from "../api_upload";
+import { list, del } from "../api_share";
+import { testLogin, testLogout } from "./api_auth_test";
+
+function verify(fileName) {
+ return list()
+ .then(list => {
+ if (list == null) {
+ throw new Error("upload: list fail");
+ }
+
+ // TODO: should verify file name
+ const filterInfo = list.find(info => {
+ return info.PathLocal.includes(fileName);
+ });
+
+ if (filterInfo == null) {
+ console.error(list);
+ throw new Error("upload: file name not found");
+ } else {
+ return filterInfo.Id;
+ }
+ })
+ .then(shareId => {
+ console.log("upload api: ok");
+ del(shareId);
+ })
+ .then(testLogout)
+ .catch(err => {
+ throw err;
+ });
+}
+
+export function testUpload(file) {
+ const onStart = () => true;
+ const onProgress = () => true;
+ const onFinish = () => true;
+ const onError = err => {
+ throw new Error(JSON.stringify(err));
+ };
+ const uploader = new FileUploader(onStart, onProgress, onFinish, onError);
+
+ return uploader.uploadFile(file).catch(err => {
+ console.error(err);
+ });
+}
+
+export function testUploadOneFile(file, fileName) {
+ const onStart = () => true;
+ const onProgress = () => true;
+ const onFinish = () => true;
+ const onError = err => {
+ throw new Error(JSON.stringify(err));
+ };
+ const uploader = new FileUploader(onStart, onProgress, onFinish, onError);
+
+ return testLogin()
+ .then(() => uploader.uploadFile(file))
+ .then(() => verify(fileName))
+ .catch(err => {
+ console.error(err);
+ });
+}
diff --git a/client/libs/utils.js b/client/libs/utils.js
new file mode 100644
index 0000000..dddbc26
--- /dev/null
+++ b/client/libs/utils.js
@@ -0,0 +1,5 @@
+export function makePostBody(paramMap) {
+ return Object.keys(paramMap)
+ .map(key => `${key}=${paramMap[key]}`)
+ .join("&");
+}
diff --git a/client/panels/admin.jsx b/client/panels/admin.jsx
new file mode 100644
index 0000000..b098b1f
--- /dev/null
+++ b/client/panels/admin.jsx
@@ -0,0 +1,150 @@
+import axios from "axios";
+import React from "react";
+import ReactDOM from "react-dom";
+
+import { config } from "../config";
+import { addLocalFiles, list } from "../libs/api_share";
+import { login, logout } from "../libs/api_auth";
+import { FilePane } from "../components/composite/file_pane";
+import { InfoBar } from "../components/composite/info_bar";
+import { Log } from "../components/composite/log";
+
+function getWidth() {
+ if (window.innerWidth >= window.innerHeight) {
+ return `${Math.floor(
+ window.innerWidth * 0.95 / config.rootSize / config.colWidth
+ ) * config.colWidth}rem`;
+ }
+ return "auto";
+}
+
+const styleLogContainer = {
+ paddingTop: "1rem",
+ textAlign: "center",
+ height: "2rem",
+ overflowX: "hidden" // TODO: should no hidden
+};
+
+const styleLogContent = {
+ color: "#333",
+ fontSize: "0.875rem",
+ opacity: 0.6,
+ backgroundColor: "#fff",
+ borderRadius: "1rem",
+ whiteSpace: "nowrap"
+};
+
+class AdminPanel extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isLogin: false,
+ filterName: "",
+ serverAddr: `${window.location.protocol}//${window.location.hostname}:${
+ window.location.port
+ }`,
+ width: getWidth()
+ };
+ this.log = {
+ ok: msg => console.log(msg),
+ warning: msg => console.log(msg),
+ info: msg => console.log(msg),
+ error: msg => console.log(msg),
+ start: msg => console.log(msg),
+ end: msg => console.log(msg)
+ };
+ this.logComponent = ;
+ }
+
+ componentWillMount() {
+ list().then(infos => {
+ if (infos != null) {
+ this.setState({ isLogin: true });
+ }
+ });
+ }
+
+ setWidth = () => {
+ this.setState({ width: getWidth() });
+ };
+
+ // componentDidMount() {
+ // window.addEventListener("resize", this.setWidth);
+ // }
+
+ // componentWillUnmount() {
+ // window.removeEventListener("resize", this.setWidth);
+ // }
+
+ onLogin = (serverAddr, adminId, adminPwd) => {
+ login(serverAddr, adminId, adminPwd).then(ok => {
+ if (ok === true) {
+ this.setState({ isLogin: true });
+ } else {
+ this.log.error("Fail to login");
+ this.setState({ isLogin: false });
+ }
+ });
+ };
+
+ onLogout = serverAddr => {
+ logout(serverAddr).then(ok => {
+ if (ok === false) {
+ this.log.error("Fail to log out");
+ } else {
+ this.log.ok("You are logged out");
+ }
+ this.setState({ isLogin: false });
+ });
+ };
+
+ onSearch = fileName => {
+ this.setState({ filterName: fileName });
+ };
+
+ assignLog = logRef => {
+ this.log = logRef;
+ this.log.info(
+
+ Know more about Quickshare
+
+ );
+ };
+
+ render() {
+ const width = this.state.width;
+
+ return (
+
+
+ {this.logComponent}
+
+ {this.state.isLogin ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+}
+
+ReactDOM.render(, document.getElementById("app"));
diff --git a/client/tests/enzyme_setup.js b/client/tests/enzyme_setup.js
new file mode 100644
index 0000000..814fd50
--- /dev/null
+++ b/client/tests/enzyme_setup.js
@@ -0,0 +1,4 @@
+import { configure } from "enzyme";
+import Adapter from "enzyme-adapter-react-15";
+
+configure({ adapter: new Adapter() });
diff --git a/client/tests/test_helper.js b/client/tests/test_helper.js
new file mode 100644
index 0000000..32d76ac
--- /dev/null
+++ b/client/tests/test_helper.js
@@ -0,0 +1,73 @@
+// function should be called after async operation is finished
+export function execFuncs(instance, execs) {
+ // instance: enzyme mounted component
+ // const execs = [
+ // {
+ // func: "componentWillMount",
+ // args: []
+ // }
+ // ];
+ return execs.reduce((prePromise, nextFunc) => {
+ return prePromise.then(() => instance[nextFunc.func](...nextFunc.args));
+ }, Promise.resolve());
+}
+
+export function execsToStr(execs) {
+ // const execs = [
+ // {
+ // func: "componentWillMount",
+ // args: []
+ // }
+ // ];
+ const execList = execs.map(
+ funcInfo => `${funcInfo.func}(${funcInfo.args.join(", ")})`
+ );
+
+ return execList.join(", ");
+}
+
+export function getDesc(componentName, testCase) {
+ // const testCase = {
+ // execs: [
+ // {
+ // func: "onAddLocalFiles",
+ // args: []
+ // }
+ // ],
+ // state: {
+ // filterFileName: ""
+ // },
+ // calls: [
+ // {
+ // func: "onAddLocalFiles",
+ // count: 1
+ // }
+ // ]
+ // }
+ return `${componentName} should satisfy following by exec ${execsToStr(
+ testCase.execs
+ )}
+ state=${JSON.stringify(testCase.state)}
+ calls=${JSON.stringify(testCase.calls)} `;
+}
+
+export function verifyCalls(calls, stubs) {
+ // const calls: [
+ // {
+ // func: "funcName",
+ // count: 1
+ // }
+ // ];
+ // const stubs = {
+ // funcName: jest.fn(),
+ // };
+ let err = null;
+ calls.forEach(called => {
+ if (stubs[called.func].mock.calls.length != called.count) {
+ err = `InfoBar: ${called.func} should be called ${called.count} but ${
+ stubs[called.func].mock.calls.length
+ }`;
+ }
+ });
+ return err;
+}
diff --git a/client/webpack.config.common.js b/client/webpack.config.common.js
new file mode 100644
index 0000000..51ea9cd
--- /dev/null
+++ b/client/webpack.config.common.js
@@ -0,0 +1,60 @@
+const webpack = require("webpack");
+const CleanWebpackPlugin = require("clean-webpack-plugin");
+// const HtmlWebpackPlugin = require("html-webpack-plugin");
+
+const outputPath = `${__dirname}/../public/dist`;
+
+module.exports = {
+ context: __dirname,
+ entry: {
+ assets: ["axios", "immutable", "react", "react-dom"],
+ admin: "./panels/admin"
+ },
+ output: {
+ path: outputPath,
+ filename: "[name].bundle.js"
+ },
+ module: {
+ rules: [
+ {
+ test: /\.js|jsx$/,
+ use: [
+ {
+ loader: "babel-loader",
+ options: {
+ presets: ["es2015", "react", "stage-2"]
+ }
+ }
+ ]
+ },
+ {
+ test: /\.css$/,
+ use: ["style-loader", "css-loader"]
+ },
+ {
+ test: /\.(png|jpg|gif)$/,
+ use: [
+ {
+ loader: "file-loader",
+ options: {}
+ }
+ ]
+ }
+ ]
+ },
+ resolve: {
+ extensions: [".js", ".json", ".jsx", ".css"]
+ },
+ plugins: [
+ new webpack.optimize.CommonsChunkPlugin({
+ name: "assets",
+ // filename: "vendor.js"
+ // (Give the chunk a different name)
+ minChunks: Infinity
+ // (with more entries, this ensures that no other module
+ // goes into the vendor chunk)
+ }),
+ // new HtmlWebpackPlugin(),
+ new CleanWebpackPlugin([outputPath])
+ ]
+};
diff --git a/client/webpack.config.dev.js b/client/webpack.config.dev.js
new file mode 100644
index 0000000..45be7e1
--- /dev/null
+++ b/client/webpack.config.dev.js
@@ -0,0 +1,17 @@
+const merge = require("webpack-merge");
+const common = require("./webpack.config.common.js");
+
+module.exports = merge(common, {
+ entry: {
+ api_test: "./libs/test/api_test"
+ },
+ devtool: "inline-source-map",
+ devServer: {
+ contentBase: "./dist"
+ },
+ watchOptions: {
+ aggregateTimeout: 1000,
+ poll: 1000,
+ ignored: /node_modules/
+ }
+});
diff --git a/client/webpack.config.prod.js b/client/webpack.config.prod.js
new file mode 100644
index 0000000..f7ffae0
--- /dev/null
+++ b/client/webpack.config.prod.js
@@ -0,0 +1,16 @@
+const common = require("./webpack.config.common.js");
+const merge = require("webpack-merge");
+const UglifyJS = require("uglifyjs-webpack-plugin");
+const webpack = require("webpack");
+
+module.exports = merge(common, {
+ devtool: "source-map",
+ plugins: [
+ new UglifyJS({
+ sourceMap: true
+ }),
+ new webpack.DefinePlugin({
+ "process.env.NODE_ENV": JSON.stringify("production")
+ })
+ ]
+});
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..5b64a7e
--- /dev/null
+++ b/config.json
@@ -0,0 +1,77 @@
+{
+ "AppName": "qs",
+ "AdminId": "admin",
+ "AdminPwd": "quicksh@re",
+ "SecretKey": "qs",
+ "Production": true,
+ "HostName": "",
+ "Port": 8888,
+ "MaxUpBytesPerSec": 2000000,
+ "MaxDownBytesPerSec": 1000000,
+ "MaxRangeLength": 10485760,
+ "Timeout": 7200000,
+ "ReadTimeout": 5000,
+ "WriteTimeout": 7200000,
+ "IdleTimeout": 10000,
+ "WorkerPoolSize": 16,
+ "TaskQueueSize": 16,
+ "QueueSize": 16,
+ "ParseFormBufSize": 5000000,
+ "MaxHeaderBytes": 1024,
+ "DownLimit": -1,
+ "MaxShares": 16384,
+ "LocalFileLimit": -1,
+ "CookieDomain": "",
+ "CookieHttpOnly": false,
+ "CookieMaxAge": 604800,
+ "CookiePath": "/",
+ "CookieSecure": false,
+ "KeyAdminId": "adminid",
+ "KeyAdminPwd": "adminpwd",
+ "KeyToken": "token",
+ "KeyFileName": "fname",
+ "KeyFileSize": "size",
+ "KeyShareId": "shareid",
+ "KeyStart": "start",
+ "KeyLen": "len",
+ "KeyChunk": "chunk",
+ "KeyAct": "act",
+ "KeyExpires": "expires",
+ "KeyDownLimit": "downlimit",
+ "ActStartUpload": "startupload",
+ "ActUpload": "upload",
+ "ActFinishUpload": "finishupload",
+ "ActLogin": "login",
+ "ActLogout": "logout",
+ "ActShadowId": "shadowid",
+ "ActPublishId": "publishid",
+ "ActSetDownLimit": "setdownlimit",
+ "ActAddLocalFiles": "addlocalfiles",
+ "AllUsers": "addlocalfiles",
+ "OpIdIpVisit": 0,
+ "OpIdUpload": 1,
+ "OpIdDownload": 2,
+ "OpIdLogin": 3,
+ "OpIdGetFInfo": 4,
+ "OpIdDelFInfo": 5,
+ "OpIdOpFInfo": 6,
+ "PathLocal": "files",
+ "PathLogin": "/login",
+ "PathDownloadLogin": "/download-login",
+ "PathDownload": "/download",
+ "PathUpload": "/upload",
+ "PathStartUpload": "/startupload",
+ "PathFinishUpload": "/finishupload",
+ "PathFileInfo": "/fileinfo",
+ "PathClient": "/",
+ "LimiterCap": 256,
+ "LimiterTtl": 3600,
+ "LimiterCyc": 1,
+ "BucketCap": 10,
+ "SpecialCapsStr": {
+ "0": 30,
+ "1": 10,
+ "2": 10,
+ "3": 1
+ }
+}
diff --git a/demo.jpg b/demo.jpg
new file mode 100644
index 0000000..5a1a1c7
Binary files /dev/null and b/demo.jpg differ
diff --git a/docs/CONFIG_en-us.md b/docs/CONFIG_en-us.md
new file mode 100644
index 0000000..7b43de2
--- /dev/null
+++ b/docs/CONFIG_en-us.md
@@ -0,0 +1,82 @@
+## Configuration
+
+```Javascript
+{
+ "AppName": "qs",
+ "AdminId": "admin", // login user name
+ "AdminPwd": "quicksh@re", // login password
+ "SecretKey": "qs", // key for hashing cookie (jwt)
+ "Production": true,
+ "HostName": "", // listening address
+ "Port": 8888, // listening port
+ "MaxUpBytesPerSec": 2000000, // upload speed limit
+ "MaxDownBytesPerSec": 1000000, // download speed limit
+ "MaxRangeLength": 10485760, // max length of chunk to upload at once
+ "Timeout": 7200000, // connection timeout
+ "ReadTimeout": 5000, // connection read request timeout
+ "WriteTimeout": 7200000, // connection write response timeout
+ "IdleTimeout": 10000, // connection idle timeout
+ "WorkerPoolSize": 16, // number of workers, it decides how many download connections are provided at same time
+ "TaskQueueSize": 16, // how many requests can be queued
+ "QueueSize": 16,
+ "ParseFormBufSize": 5000000, // buffer for parsing request
+ "MaxHeaderBytes": 1024, // max header size in byte
+ "DownLimit": -1, // default download limit
+ "MaxShares": 16384, // max number of sharing
+ "LocalFileLimit": -1, // max number of listing file at once
+ "CookieDomain": "",
+ "CookieHttpOnly": false,
+ "CookieMaxAge": 604800,
+ "CookiePath": "/",
+ "CookieSecure": false,
+ "KeyAdminId": "adminid",
+ "KeyAdminPwd": "adminpwd",
+ "KeyToken": "token",
+ "KeyFileName": "fname",
+ "KeyFileSize": "size",
+ "KeyShareId": "shareid",
+ "KeyStart": "start",
+ "KeyLen": "len",
+ "KeyChunk": "chunk",
+ "KeyAct": "act",
+ "KeyExpires": "expires",
+ "KeyDownLimit": "downlimit",
+ "ActStartUpload": "startupload",
+ "ActUpload": "upload",
+ "ActFinishUpload": "finishupload",
+ "ActLogin": "login",
+ "ActLogout": "logout",
+ "ActShadowId": "shadowid",
+ "ActPublishId": "publishid",
+ "ActSetDownLimit": "setdownlimit",
+ "ActAddLocalFiles": "addlocalfiles",
+ "AllUsers": "addlocalfiles",
+ "OpIdIpVisit": 0,
+ "OpIdUpload": 1,
+ "OpIdDownload": 2,
+ "OpIdLogin": 3,
+ "OpIdGetFInfo": 4,
+ "OpIdDelFInfo": 5,
+ "OpIdOpFInfo": 6,
+ "PathLocal": "files",
+ "PathLogin": "/login",
+ "PathDownloadLogin": "/download-login",
+ "PathDownload": "/download",
+ "PathUpload": "/upload",
+ "PathStartUpload": "/startupload",
+ "PathFinishUpload": "/finishupload",
+ "PathFileInfo": "/fileinfo",
+ "PathClient": "/",
+ "LimiterCap": 256,
+ "LimiterTtl": 3600,
+ "LimiterCyc": 1,
+ "BucketCap": 10, // operation is allowed at most 10 times per second, but SpecialCapsStr will override this value
+ "SpecialCapsStr": {
+ "0": 30, // IpVisit is allowed at most 30 times per second
+ "1": 10, // Uploading is allowed at most 10 times per second
+ "2": 10, // Downloading is allowed at most 10 times per second
+ "3": 1 // Login/Logout is allowed at most 1 time per second
+ // You can also add rate limits according to OpIdxxx above.
+ }
+}
+```
diff --git a/docs/FAQ_en-us.md b/docs/FAQ_en-us.md
new file mode 100644
index 0000000..3290f00
--- /dev/null
+++ b/docs/FAQ_en-us.md
@@ -0,0 +1,18 @@
+## FAQ
+
+* How to change accound name and password?
+ * Go to quickshare folder
+ * Open config.json using text editor. (e.g. notpad++, sublime, vscode, etc...)
+ * Search for line `"AdminPwd": "quicksh@re",`
+ * Replace `quicksh@re` with your password, e.g. `"AdminPwd": "myPassword",`
+ * Then you can also update user name `"AdminId": "myUserName",` in above way.
+* How to change listening address(or port)?
+ * Go to quickshare folder
+ * Open config.json using text editor. (e.g. notpad++, sublime, vscode, etc...)
+ * Search for line `"HostName": "",`
+ * Change the value of `HostName`, e.g. `"HostName": "192.168.0.6",`
+ * You can also change port value (`"Port": 8888,`) in above way
+* How to change background?
+ * Go to `public` folder under quickshare folder
+ * Open style.css using text editor. (e.g. notpad++, sublime, vscode, etc...)
+ * Update `body`'s css.
diff --git a/docs/FAQ_zh-cn.md b/docs/FAQ_zh-cn.md
new file mode 100644
index 0000000..9edf39f
--- /dev/null
+++ b/docs/FAQ_zh-cn.md
@@ -0,0 +1,17 @@
+## 常见问题
+
+* 怎么改用户名和密码?
+ * 进入 quickshare 文件夹
+ * 用文本编辑器打开 config.json. (比如 notpad++, sublime, vscode, 等等...)
+ * 查找行 `"AdminPwd": "quicksh@re",`
+ * 用你的密码替换 `quicksh@re`, 比如 `"AdminPwd": "你的密码",`
+* 怎么改监听地址(端口)?
+ * 进入 quickshare 文件夹
+ * 用文本编辑器打开 config.json. (比如 notpad++, sublime, vscode, 等等...)
+ * 查找行 `"HostName": "",`
+ * 更改`HostName`的值, 比如 `"HostName": "192.168.0.6",`
+ * 你可以用上面的方法更改端口值(`"Port": 8888,`)
+* 怎么更改背景?
+ * 进入 quickshare 文件夹下的`public`文件夹
+ * 使用文本编辑器打开 style.css. (比如 notpad++, sublime, vscode, 等等...)
+ * 更新 `body`'的 css
diff --git a/docs/README_zh-cn.md b/docs/README_zh-cn.md
new file mode 100644
index 0000000..fcaa026
--- /dev/null
+++ b/docs/README_zh-cn.md
@@ -0,0 +1,69 @@
+
+ Quickshare
+
+
+ 一个小而美的文件共享服务器
+
+
+
+
+
+
+
+
+
+选择语言: [English](../README.md) | 简体中文
+
+## 下载
+
+| Linux | Mac | Windows |
+| -------- | -------- | -------- |
+| [下载]() | [下载]() | [下载]() |
+
+## 特点
+
+* 在浏览器中上传下载, 无需安装任何客户端
+* 在桌面电脑和移动设备件间共享文件
+* 绿色软件
+* 支持添加本地文件
+* 支持添加文件下载次数上限
+* 支持断点续传
+
+## 安装
+
+开始使用 quickshare 主要需要两步: 解压, 启动.
+
+第一步, 解压下载包, 启动 quickshare.
+
+### Linux
+
+* 解压包: `unzip [package].` (`[package]` 可以是`quickshare_0.0.8_linux_x86_6 4.zip`)
+* 进入文件夹, 启动 quickshare `./quickshare`
+
+### Mac
+
+* 解压包: `unzip [package].` (`[package]` 可以是`quickshare_0.0.8_macos_x86_6 4.zip`)
+* 进入文件夹, 启动 quickshare `./quickshare`
+
+### Windows
+
+* 解压包
+* 进入文件夹, 双击 `quickshare.exe` 启动 quickshare
+
+最后一步, 在浏览器中面基 quickshare.
+
+* Quickshare 会在启动后, 在命令中输出中显示 `quickshare starts @ [URL]` (举个栗子 `URL` 可以是 `192.168.0.1:8888`)
+* 在浏览器中打开 `URL` 并使用 `admin` 和 `quicksh@re` 登入
+* 开耍(不过别忘改密码[常见问题文档](./FAQ_zh-cn.md))
+
+### 常见问题
+
+请参考[常见问题文档](./FAQ_zh-cn.md)
+
+### 配置
+
+请参考[配置文档](./CONFIG_en-us.md)
+
+### 贡献代码
+
+待添加...
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8536fae
--- /dev/null
+++ b/package.json
@@ -0,0 +1,68 @@
+{
+ "name": "quickshare",
+ "version": "0.0.11",
+ "description": "A succinct file sharing server.",
+ "main": "",
+ "scripts": {
+ "start": "yarn build-client && go run server.go",
+ "test": "go test -v ./... && yarn jest --coverage",
+ "fmt": "go fmt ./...",
+ "clean": "go clean && rm -r ./dist && rm -r ./public/dist",
+ "build":
+ "rm -rf ./dist && rm -rf ./public/dist && yarn build-client && goreleaser --snapshot",
+ "build-client": "webpack --config ./client/webpack.config.prod.js",
+ "build-client-dev": "webpack --config ./client/webpack.config.dev.js"
+ },
+ "author": "hexxa",
+ "license": "ISC",
+ "devDependencies": {
+ "babel-core": "^6.26.0",
+ "babel-jest": "^21.2.0",
+ "babel-loader": "^6.4.1",
+ "babel-preset-env": "^1.2.2",
+ "babel-preset-es2015": "^6.24.0",
+ "babel-preset-react": "^6.24.1",
+ "babel-preset-stage-0": "^6.24.1",
+ "babel-preset-stage-2": "^6.24.1",
+ "clean-webpack-plugin": "^0.1.19",
+ "css-loader": "^0.28.11",
+ "enzyme": "^3.3.0",
+ "enzyme-adapter-react-15": "^1.0.5",
+ "eslint": "^4.12.0",
+ "eslint-config-airbnb": "^16.0.0",
+ "eslint-plugin-import": "^2.7.0",
+ "eslint-plugin-jsx-a11y": "^6.0.2",
+ "eslint-plugin-react": "^7.4.0",
+ "html-webpack-plugin": "^3.2.0",
+ "jest": "^21.2.1",
+ "md5": "^2.2.1",
+ "react-addons-test-utils": "^15.6.2",
+ "react-test-renderer": "15",
+ "regenerator-runtime": "^0.11.1",
+ "style-loader": "^0.21.0",
+ "uglifyjs-webpack-plugin": "^1.2.5",
+ "uuid": "^3.1.0",
+ "value-equal": "^0.4.0",
+ "webpack": "^2.0.0",
+ "webpack-dev-server": "^2.9.5"
+ },
+ "dependencies": {
+ "axios": "^0.16.2",
+ "byte-size": "^4.0.2",
+ "debounce": "^1.1.0",
+ "file-loader": "^1.1.11",
+ "immutable": "^3.8.2",
+ "lodash.throttle": "^4.1.1",
+ "react": "^15.6.2",
+ "react-dom": "^15.6.2",
+ "react-icons": "^2.2.7",
+ "webpack-merge": "^4.1.2"
+ },
+ "jest": {
+ "transform": {
+ "^.+\\.jsx?$": "babel-jest"
+ },
+ "setupFiles": ["./client/tests/enzyme_setup.js"],
+ "verbose": true
+ }
+}
diff --git a/public/api_test.html b/public/api_test.html
new file mode 100644
index 0000000..45f7f60
--- /dev/null
+++ b/public/api_test.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/ggb.jpg b/public/ggb.jpg
new file mode 100644
index 0000000..3176036
Binary files /dev/null and b/public/ggb.jpg differ
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..74e1dab
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/squared_metal.png b/public/squared_metal.png
new file mode 100644
index 0000000..771231f
Binary files /dev/null and b/public/squared_metal.png differ
diff --git a/public/style.css b/public/style.css
new file mode 100644
index 0000000..cfd36b3
--- /dev/null
+++ b/public/style.css
@@ -0,0 +1,68 @@
+html {
+ background: url("ggb.jpg") no-repeat left center fixed;
+ background-size: auto 100%;
+ /* background-image: url("squared_metal.png"); repeat fixed*/
+ background-color: black;
+ height: 100%;
+}
+
+body {
+ height: 100%;
+ min-height: 100%;
+ color: #333;
+ font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif;
+ font-size: 16px;
+ margin: 0;
+ padding: 0;
+ outline: none;
+ border: none;
+}
+
+a {
+ outline: none;
+ border: none;
+}
+
+.anm-rotate {
+ animation: rotate 1s infinite linear;
+}
+
+@keyframes rotate {
+ 20% {
+ transform: rotate(72deg);
+ }
+ 40% {
+ transform: rotate(144deg);
+ }
+ 60% {
+ transform: rotate(216deg);
+ }
+ 80% {
+ transform: rotate(288deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.anm-lighter:hover {
+ animation: lighter 0.5 1;
+}
+
+@keyframes lighter {
+ 0% {
+ opacity: 100%;
+ }
+ 50% {
+ opacity: 50%;
+ }
+}
+
+::-moz-selection {
+ background: #2ecc71;
+ color: #fff;
+}
+::selection {
+ background: #2ecc71;
+ color: #fff;
+}
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..26b3f49
--- /dev/null
+++ b/server.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "time"
+)
+
+import (
+ "quickshare/server/apis"
+ "quickshare/server/libs/cfg"
+)
+
+func main() {
+ config := cfg.NewConfigFrom("config.json")
+ srvShare := apis.NewSrvShare(config)
+
+ // TODO: using httprouter instead
+ mux := http.NewServeMux()
+ mux.HandleFunc(config.PathLogin, srvShare.LoginHandler)
+ mux.HandleFunc(config.PathStartUpload, srvShare.StartUploadHandler)
+ mux.HandleFunc(config.PathUpload, srvShare.UploadHandler)
+ mux.HandleFunc(config.PathFinishUpload, srvShare.FinishUploadHandler)
+ mux.HandleFunc(config.PathDownload, srvShare.DownloadHandler)
+ mux.HandleFunc(config.PathFileInfo, srvShare.FileInfoHandler)
+ mux.HandleFunc(config.PathClient, srvShare.ClientHandler)
+
+ server := &http.Server{
+ Addr: fmt.Sprintf("%s:%d", config.HostName, config.Port),
+ Handler: mux,
+ MaxHeaderBytes: config.MaxHeaderBytes,
+ ReadTimeout: time.Duration(config.ReadTimeout) * time.Millisecond,
+ WriteTimeout: time.Duration(config.WriteTimeout) * time.Millisecond,
+ IdleTimeout: time.Duration(config.IdleTimeout) * time.Millisecond,
+ }
+
+ log.Printf("quickshare starts @ %s:%d", config.HostName, config.Port)
+ log.Fatal(server.ListenAndServe())
+}
diff --git a/server/apis/auth.go b/server/apis/auth.go
new file mode 100644
index 0000000..ff5afd5
--- /dev/null
+++ b/server/apis/auth.go
@@ -0,0 +1,105 @@
+package apis
+
+import (
+ "net/http"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/httputil"
+ "quickshare/server/libs/httpworker"
+)
+
+func (srv *SrvShare) LoginHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodPost {
+ srv.Http.Fill(httputil.Err404, res)
+ return
+ }
+
+ act := req.FormValue(srv.Conf.KeyAct)
+ todo := func(res http.ResponseWriter, req *http.Request) interface{} { return httputil.Err404 }
+ switch act {
+ case srv.Conf.ActLogin:
+ todo = srv.Login
+ case srv.Conf.ActLogout:
+ todo = srv.Logout
+ default:
+ srv.Http.Fill(httputil.Err404, res)
+ return
+ }
+
+ ack := make(chan error, 1)
+ ok := srv.WorkerPool.Put(&httpworker.Task{
+ Ack: ack,
+ Do: srv.Wrap(todo),
+ Res: res,
+ Req: req,
+ })
+ if !ok {
+ srv.Http.Fill(httputil.Err503, res)
+ return
+ }
+
+ execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
+ if srv.Err.IsErr(execErr) {
+ srv.Http.Fill(httputil.Err500, res)
+ }
+}
+
+func (srv *SrvShare) Login(res http.ResponseWriter, req *http.Request) interface{} {
+ // all users need to pass same wall to login
+ if !srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr)) ||
+ !srv.Walls.PassOpLimit(srv.Conf.AllUsers, srv.Conf.OpIdLogin) {
+ return httputil.Err504
+ }
+
+ return srv.login(
+ req.FormValue(srv.Conf.KeyAdminId),
+ req.FormValue(srv.Conf.KeyAdminPwd),
+ res,
+ )
+}
+
+func (srv *SrvShare) login(adminId string, adminPwd string, res http.ResponseWriter) interface{} {
+ if adminId != srv.Conf.AdminId ||
+ adminPwd != srv.Conf.AdminPwd {
+ return httputil.Err401
+ }
+
+ token := srv.Walls.MakeLoginToken(srv.Conf.AdminId)
+ if token == "" {
+ return httputil.Err500
+ }
+
+ srv.Http.SetCookie(res, srv.Conf.KeyToken, token)
+ return httputil.Ok200
+}
+
+func (srv *SrvShare) Logout(res http.ResponseWriter, req *http.Request) interface{} {
+ srv.Http.SetCookie(res, srv.Conf.KeyToken, "-")
+ return httputil.Ok200
+}
+
+func (srv *SrvShare) IsValidLength(length int64) bool {
+ return length > 0 && length <= srv.Conf.MaxUpBytesPerSec
+}
+
+func (srv *SrvShare) IsValidStart(start, expectStart int64) bool {
+ return start == expectStart
+}
+
+func (srv *SrvShare) IsValidShareId(shareId string) bool {
+ // id could be 0 for dev environment
+ if srv.Conf.Production {
+ return len(shareId) == 64
+ }
+ return true
+}
+
+func (srv *SrvShare) IsValidDownLimit(limit int) bool {
+ return limit >= -1
+}
+
+func IsValidFileName(fileName string) bool {
+ return fileName != "" && len(fileName) < 240
+}
diff --git a/server/apis/auth_test.go b/server/apis/auth_test.go
new file mode 100644
index 0000000..e46375b
--- /dev/null
+++ b/server/apis/auth_test.go
@@ -0,0 +1,78 @@
+package apis
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+)
+
+import (
+ "quickshare/server/libs/cfg"
+ "quickshare/server/libs/encrypt"
+ "quickshare/server/libs/httputil"
+)
+
+func TestLogin(t *testing.T) {
+ conf := cfg.NewConfig()
+
+ type testCase struct {
+ Desc string
+ AdminId string
+ AdminPwd string
+ Result interface{}
+ VerifyToken bool
+ }
+
+ testCases := []testCase{
+ testCase{
+ Desc: "invalid input",
+ AdminId: "",
+ AdminPwd: "",
+ Result: httputil.Err401,
+ VerifyToken: false,
+ },
+ testCase{
+ Desc: "account not match",
+ AdminId: "unknown",
+ AdminPwd: "unknown",
+ Result: httputil.Err401,
+ VerifyToken: false,
+ },
+ testCase{
+ Desc: "succeed to login",
+ AdminId: conf.AdminId,
+ AdminPwd: conf.AdminPwd,
+ Result: httputil.Ok200,
+ VerifyToken: true,
+ },
+ }
+
+ for _, testCase := range testCases {
+ srv := NewSrvShare(conf)
+ res := &stubWriter{Headers: map[string][]string{}}
+ ret := srv.login(testCase.AdminId, testCase.AdminPwd, res)
+
+ if ret != testCase.Result {
+ t.Fatalf("login: reponse=%v testCase=%v", ret, testCase.Result)
+ }
+
+ // verify cookie (only token.adminid part))
+ if testCase.VerifyToken {
+ cookieVal := strings.Replace(
+ res.Header().Get("Set-Cookie"),
+ fmt.Sprintf("%s=", conf.KeyToken),
+ "",
+ 1,
+ )
+
+ gotTokenStr := strings.Split(cookieVal, ";")[0]
+ token := encrypt.JwtEncrypterMaker(conf.SecretKey)
+ token.FromStr(gotTokenStr)
+ gotToken, found := token.Get(conf.KeyAdminId)
+ if !found || conf.AdminId != gotToken {
+ t.Fatalf("login: token admin id unmatch got=%v expect=%v", gotToken, conf.AdminId)
+ }
+ }
+
+ }
+}
diff --git a/server/apis/client.go b/server/apis/client.go
new file mode 100644
index 0000000..63f6724
--- /dev/null
+++ b/server/apis/client.go
@@ -0,0 +1,66 @@
+package apis
+
+import (
+ "net/http"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/httputil"
+ "quickshare/server/libs/httpworker"
+)
+
+func (srv *SrvShare) ClientHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodGet {
+ srv.Http.Fill(httputil.Err404, res)
+ return
+ }
+
+ ack := make(chan error, 1)
+ ok := srv.WorkerPool.Put(&httpworker.Task{
+ Ack: ack,
+ Do: srv.Wrap(srv.GetClient),
+ Res: res,
+ Req: req,
+ })
+ if !ok {
+ srv.Http.Fill(httputil.Err503, res)
+ return
+ }
+
+ execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
+ if srv.Err.IsErr(execErr) {
+ srv.Http.Fill(httputil.Err500, res)
+ }
+}
+
+func (srv *SrvShare) GetClient(res http.ResponseWriter, req *http.Request) interface{} {
+ if !srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr)) {
+ return httputil.Err504
+ }
+
+ return srv.getClient(res, req, req.URL.EscapedPath())
+}
+
+func (srv *SrvShare) getClient(res http.ResponseWriter, req *http.Request, relPath string) interface{} {
+ if strings.HasSuffix(relPath, "/") {
+ relPath = relPath + "index.html"
+ }
+ if !IsValidClientPath(relPath) {
+ return httputil.Err400
+ }
+
+ fullPath := filepath.Clean(filepath.Join("./public", relPath))
+ http.ServeFile(res, req, fullPath)
+ return 0
+}
+
+func IsValidClientPath(fullPath string) bool {
+ if strings.Contains(fullPath, "..") {
+ return false
+ }
+
+ return true
+}
diff --git a/server/apis/download.go b/server/apis/download.go
new file mode 100644
index 0000000..9174786
--- /dev/null
+++ b/server/apis/download.go
@@ -0,0 +1,69 @@
+package apis
+
+import (
+ "net/http"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/fileidx"
+ "quickshare/server/libs/httputil"
+ "quickshare/server/libs/httpworker"
+)
+
+func (srv *SrvShare) DownloadHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodGet {
+ srv.Http.Fill(httputil.Err404, res)
+ }
+
+ ack := make(chan error, 1)
+ ok := srv.WorkerPool.Put(&httpworker.Task{
+ Ack: ack,
+ Do: srv.Wrap(srv.Download),
+ Res: res,
+ Req: req,
+ })
+ if !ok {
+ srv.Http.Fill(httputil.Err503, res)
+ }
+
+ // using WriteTimeout instead of Timeout
+ // After timeout, connection will be lost, and worker will fail to write and return
+ execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.WriteTimeout)*time.Millisecond)
+ if srv.Err.IsErr(execErr) {
+ srv.Http.Fill(httputil.Err500, res)
+ }
+}
+
+func (srv *SrvShare) Download(res http.ResponseWriter, req *http.Request) interface{} {
+ shareId := req.FormValue(srv.Conf.KeyShareId)
+ if !srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr)) ||
+ !srv.Walls.PassOpLimit(shareId, srv.Conf.OpIdDownload) {
+ return httputil.Err429
+ }
+
+ return srv.download(shareId, res, req)
+}
+
+func (srv *SrvShare) download(shareId string, res http.ResponseWriter, req *http.Request) interface{} {
+ if !srv.IsValidShareId(shareId) {
+ return httputil.Err400
+ }
+
+ fileInfo, found := srv.Index.Get(shareId)
+ switch {
+ case !found || fileInfo.State != fileidx.StateDone:
+ return httputil.Err404
+ case fileInfo.DownLimit == 0:
+ return httputil.Err412
+ default:
+ updated, _ := srv.Index.DecrDownLimit(shareId)
+ if updated != 1 {
+ return httputil.Err500
+ }
+ }
+
+ err := srv.Downloader.ServeFile(res, req, fileInfo)
+ srv.Err.IsErr(err)
+ return 0
+}
diff --git a/server/apis/download_test.go b/server/apis/download_test.go
new file mode 100644
index 0000000..8317d31
--- /dev/null
+++ b/server/apis/download_test.go
@@ -0,0 +1,271 @@
+package apis
+
+import (
+ "net/http"
+ "os"
+ "testing"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/cfg"
+ "quickshare/server/libs/errutil"
+ "quickshare/server/libs/fileidx"
+ "quickshare/server/libs/httputil"
+ "quickshare/server/libs/logutil"
+ "quickshare/server/libs/qtube"
+)
+
+func initServiceForDownloadTest(config *cfg.Config, indexMap map[string]*fileidx.FileInfo, content string) *SrvShare {
+ setDownloader := func(srv *SrvShare) {
+ srv.Downloader = stubDownloader{Content: content}
+ }
+
+ setIndex := func(srv *SrvShare) {
+ srv.Index = fileidx.NewMemFileIndexWithMap(len(indexMap), indexMap)
+ }
+
+ setFs := func(srv *SrvShare) {
+ srv.Fs = &stubFsUtil{
+ MockFile: &qtube.StubFile{
+ Content: content,
+ Offset: 0,
+ },
+ }
+ }
+
+ logger := logutil.NewSlog(os.Stdout, config.AppName)
+ setLog := func(srv *SrvShare) {
+ srv.Log = logger
+ }
+
+ setErr := func(srv *SrvShare) {
+ srv.Err = errutil.NewErrChecker(!config.Production, logger)
+ }
+
+ return InitSrvShare(config, setDownloader, setIndex, setFs, setLog, setErr)
+}
+
+func TestDownload(t *testing.T) {
+ conf := cfg.NewConfig()
+ conf.Production = false
+
+ type Init struct {
+ Content string
+ IndexMap map[string]*fileidx.FileInfo
+ }
+ type Input struct {
+ ShareId string
+ }
+ type Output struct {
+ IndexMap map[string]*fileidx.FileInfo
+ Response interface{}
+ Body string
+ }
+ type testCase struct {
+ Desc string
+ Init
+ Input
+ Output
+ }
+
+ testCases := []testCase{
+ testCase{
+ Desc: "empty file index",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ Response: httputil.Err404,
+ },
+ },
+ testCase{
+ Desc: "file info not found",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "1": &fileidx.FileInfo{},
+ },
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "1": &fileidx.FileInfo{},
+ },
+ Response: httputil.Err404,
+ },
+ },
+ testCase{
+ Desc: "file not found because of state=uploading",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ DownLimit: 1,
+ ModTime: time.Now().UnixNano(),
+ PathLocal: "path",
+ State: fileidx.StateUploading,
+ Uploaded: 1,
+ },
+ },
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ DownLimit: 1,
+ ModTime: time.Now().UnixNano(),
+ PathLocal: "path",
+ State: fileidx.StateUploading,
+ Uploaded: 1,
+ },
+ },
+ Response: httputil.Err404,
+ },
+ },
+ testCase{
+ Desc: "download failed because download limit = 0",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ DownLimit: 0,
+ ModTime: time.Now().UnixNano(),
+ PathLocal: "path",
+ State: fileidx.StateDone,
+ Uploaded: 1,
+ },
+ },
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ DownLimit: 0,
+ ModTime: time.Now().UnixNano(),
+ PathLocal: "path",
+ State: fileidx.StateDone,
+ Uploaded: 1,
+ },
+ },
+ Response: httputil.Err412,
+ },
+ },
+ testCase{
+ Desc: "succeed to download",
+ Init: Init{
+ Content: "content",
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ DownLimit: 1,
+ ModTime: time.Now().UnixNano(),
+ PathLocal: "path",
+ State: fileidx.StateDone,
+ Uploaded: 1,
+ },
+ },
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ DownLimit: 0,
+ ModTime: time.Now().UnixNano(),
+ PathLocal: "path",
+ State: fileidx.StateDone,
+ Uploaded: 1,
+ },
+ },
+ Response: 0,
+ Body: "content",
+ },
+ },
+ testCase{
+ Desc: "succeed to download DownLimit == -1",
+ Init: Init{
+ Content: "content",
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ DownLimit: -1,
+ ModTime: time.Now().UnixNano(),
+ PathLocal: "path",
+ State: fileidx.StateDone,
+ Uploaded: 1,
+ },
+ },
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ DownLimit: -1,
+ ModTime: time.Now().UnixNano(),
+ PathLocal: "path",
+ State: fileidx.StateDone,
+ Uploaded: 1,
+ },
+ },
+ Response: 0,
+ Body: "content",
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ srv := initServiceForDownloadTest(conf, testCase.Init.IndexMap, testCase.Content)
+ writer := &stubWriter{Headers: map[string][]string{}}
+ response := srv.download(
+ testCase.ShareId,
+ writer,
+ &http.Request{},
+ )
+
+ // verify downlimit
+ if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
+ info, _ := srv.Index.Get(testCase.ShareId)
+ t.Fatalf(
+ "download: index incorrect got=%v want=%v",
+ info,
+ testCase.Output.IndexMap[testCase.ShareId],
+ )
+ }
+
+ // verify response
+ if response != testCase.Output.Response {
+ t.Fatalf(
+ "download: response incorrect response=%v testCase=%v",
+ response,
+ testCase.Output.Response,
+ )
+ }
+
+ // verify writerContent
+ if string(writer.Response) != testCase.Output.Body {
+ t.Fatalf(
+ "download: body incorrect got=%v want=%v",
+ string(writer.Response),
+ testCase.Output.Body,
+ )
+ }
+
+ }
+}
diff --git a/server/apis/file_info.go b/server/apis/file_info.go
new file mode 100644
index 0000000..a6ad0d4
--- /dev/null
+++ b/server/apis/file_info.go
@@ -0,0 +1,234 @@
+package apis
+
+import (
+ "fmt"
+ "math/rand"
+ "net/http"
+ "path/filepath"
+ "strconv"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/fileidx"
+ "quickshare/server/libs/httputil"
+ "quickshare/server/libs/httpworker"
+)
+
+func (srv *SrvShare) FileInfoHandler(res http.ResponseWriter, req *http.Request) {
+ tokenStr := srv.Http.GetCookie(req.Cookies(), srv.Conf.KeyToken)
+ if !srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr)) ||
+ !srv.Walls.PassLoginCheck(tokenStr, req) {
+ srv.Http.Fill(httputil.Err429, res)
+ return
+ }
+
+ todo := func(res http.ResponseWriter, req *http.Request) interface{} { return httputil.Err404 }
+ switch req.Method {
+ case http.MethodGet:
+ todo = srv.List
+ case http.MethodDelete:
+ todo = srv.Del
+ case http.MethodPatch:
+ act := req.FormValue(srv.Conf.KeyAct)
+ switch act {
+ case srv.Conf.ActShadowId:
+ todo = srv.ShadowId
+ case srv.Conf.ActPublishId:
+ todo = srv.PublishId
+ case srv.Conf.ActSetDownLimit:
+ todo = srv.SetDownLimit
+ case srv.Conf.ActAddLocalFiles:
+ todo = srv.AddLocalFiles
+ default:
+ srv.Http.Fill(httputil.Err404, res)
+ return
+ }
+ default:
+ srv.Http.Fill(httputil.Err404, res)
+ return
+ }
+
+ ack := make(chan error, 1)
+ ok := srv.WorkerPool.Put(&httpworker.Task{
+ Ack: ack,
+ Do: srv.Wrap(todo),
+ Res: res,
+ Req: req,
+ })
+ if !ok {
+ srv.Http.Fill(httputil.Err503, res)
+ }
+
+ execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
+ if srv.Err.IsErr(execErr) {
+ srv.Http.Fill(httputil.Err500, res)
+ }
+}
+
+type ResInfos struct {
+ List []*fileidx.FileInfo
+}
+
+func (srv *SrvShare) List(res http.ResponseWriter, req *http.Request) interface{} {
+ if !srv.Walls.PassOpLimit(srv.Conf.AllUsers, srv.Conf.OpIdGetFInfo) {
+ return httputil.Err429
+ }
+
+ return srv.list()
+}
+
+func (srv *SrvShare) list() interface{} {
+ infos := make([]*fileidx.FileInfo, 0)
+ for _, info := range srv.Index.List() {
+ infos = append(infos, info)
+ }
+
+ return &ResInfos{List: infos}
+}
+
+func (srv *SrvShare) Del(res http.ResponseWriter, req *http.Request) interface{} {
+ shareId := req.FormValue(srv.Conf.KeyShareId)
+ if !srv.Walls.PassOpLimit(shareId, srv.Conf.OpIdDelFInfo) {
+ return httputil.Err504
+ }
+
+ return srv.del(shareId)
+}
+
+func (srv *SrvShare) del(shareId string) interface{} {
+ if !srv.IsValidShareId(shareId) {
+ return httputil.Err400
+ }
+
+ fileInfo, found := srv.Index.Get(shareId)
+ if !found {
+ return httputil.Err404
+ }
+
+ srv.Index.Del(shareId)
+ fullPath := filepath.Join(srv.Conf.PathLocal, fileInfo.PathLocal)
+ if !srv.Fs.DelFile(fullPath) {
+ // TODO: may log file name because file not exist or delete is not authenticated
+ return httputil.Err500
+ }
+
+ return httputil.Ok200
+}
+
+func (srv *SrvShare) ShadowId(res http.ResponseWriter, req *http.Request) interface{} {
+ if !srv.Walls.PassOpLimit(srv.Conf.AllUsers, srv.Conf.OpIdOpFInfo) {
+ return httputil.Err429
+ }
+
+ shareId := req.FormValue(srv.Conf.KeyShareId)
+ return srv.shadowId(shareId)
+}
+
+func (srv *SrvShare) shadowId(shareId string) interface{} {
+ if !srv.IsValidShareId(shareId) {
+ return httputil.Err400
+ }
+
+ info, found := srv.Index.Get(shareId)
+ if !found {
+ return httputil.Err404
+ }
+
+ secretId := srv.Encryptor.Encrypt(
+ []byte(fmt.Sprintf("%s%s", info.PathLocal, genPwd())),
+ )
+ if !srv.Index.SetId(info.Id, secretId) {
+ return httputil.Err412
+ }
+
+ return &ShareInfo{ShareId: secretId}
+}
+
+func (srv *SrvShare) PublishId(res http.ResponseWriter, req *http.Request) interface{} {
+ if !srv.Walls.PassOpLimit(srv.Conf.AllUsers, srv.Conf.OpIdOpFInfo) {
+ return httputil.Err429
+ }
+
+ shareId := req.FormValue(srv.Conf.KeyShareId)
+ return srv.publishId(shareId)
+}
+
+func (srv *SrvShare) publishId(shareId string) interface{} {
+ if !srv.IsValidShareId(shareId) {
+ return httputil.Err400
+ }
+
+ info, found := srv.Index.Get(shareId)
+ if !found {
+ return httputil.Err404
+ }
+
+ publicId := srv.Encryptor.Encrypt([]byte(info.PathLocal))
+ if !srv.Index.SetId(info.Id, publicId) {
+ return httputil.Err412
+ }
+
+ return &ShareInfo{ShareId: publicId}
+}
+
+func (srv *SrvShare) SetDownLimit(res http.ResponseWriter, req *http.Request) interface{} {
+ if !srv.Walls.PassOpLimit(srv.Conf.AllUsers, srv.Conf.OpIdOpFInfo) {
+ return httputil.Err429
+ }
+
+ shareId := req.FormValue(srv.Conf.KeyShareId)
+ downLimit64, downLimitParseErr := strconv.ParseInt(req.FormValue(srv.Conf.KeyDownLimit), 10, 32)
+ downLimit := int(downLimit64)
+ if srv.Err.IsErr(downLimitParseErr) {
+ return httputil.Err400
+ }
+
+ return srv.setDownLimit(shareId, downLimit)
+}
+
+func (srv *SrvShare) setDownLimit(shareId string, downLimit int) interface{} {
+ if !srv.IsValidShareId(shareId) || !srv.IsValidDownLimit(downLimit) {
+ return httputil.Err400
+ }
+
+ if !srv.Index.SetDownLimit(shareId, downLimit) {
+ return httputil.Err404
+ }
+ return httputil.Ok200
+}
+
+func (srv *SrvShare) AddLocalFiles(res http.ResponseWriter, req *http.Request) interface{} {
+ return srv.AddLocalFilesImp()
+}
+
+func (srv *SrvShare) AddLocalFilesImp() interface{} {
+ infos, err := srv.Fs.Readdir(srv.Conf.PathLocal, srv.Conf.LocalFileLimit)
+ if srv.Err.IsErr(err) {
+ panic(fmt.Sprintf("fail to readdir: %v", err))
+ }
+
+ for _, info := range infos {
+ info.DownLimit = srv.Conf.DownLimit
+ info.State = fileidx.StateDone
+ info.PathLocal = info.PathLocal
+ info.Id = srv.Encryptor.Encrypt([]byte(info.PathLocal))
+
+ addRet := srv.Index.Add(info)
+ switch {
+ case addRet == 0 || addRet == -1:
+ // TODO: return files not added
+ continue
+ case addRet == 1:
+ break
+ default:
+ return httputil.Err500
+ }
+ }
+
+ return httputil.Ok200
+}
+
+func genPwd() string {
+ return fmt.Sprintf("%d%d%d%d", rand.Intn(10), rand.Intn(10), rand.Intn(10), rand.Intn(10))
+}
diff --git a/server/apis/file_info_test.go b/server/apis/file_info_test.go
new file mode 100644
index 0000000..67733cb
--- /dev/null
+++ b/server/apis/file_info_test.go
@@ -0,0 +1,584 @@
+package apis
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+import (
+ "quickshare/server/libs/cfg"
+ "quickshare/server/libs/errutil"
+ "quickshare/server/libs/fileidx"
+ "quickshare/server/libs/httputil"
+ "quickshare/server/libs/logutil"
+)
+
+const mockShadowId = "shadowId"
+const mockPublicId = "publicId"
+
+func initServiceForFileInfoTest(
+ config *cfg.Config,
+ indexMap map[string]*fileidx.FileInfo,
+ useShadowEnc bool,
+ localFileInfos []*fileidx.FileInfo,
+) *SrvShare {
+ setIndex := func(srv *SrvShare) {
+ srv.Index = fileidx.NewMemFileIndexWithMap(len(indexMap), indexMap)
+ }
+
+ setFs := func(srv *SrvShare) {
+ srv.Fs = &stubFsUtil{MockLocalFileInfos: localFileInfos}
+ }
+
+ logger := logutil.NewSlog(os.Stdout, config.AppName)
+ setLog := func(srv *SrvShare) {
+ srv.Log = logger
+ }
+
+ errChecker := errutil.NewErrChecker(!config.Production, logger)
+ setErr := func(srv *SrvShare) {
+ srv.Err = errChecker
+ }
+
+ var setEncryptor AddDep
+ if useShadowEnc {
+ setEncryptor = func(srv *SrvShare) {
+ srv.Encryptor = &stubEncryptor{MockResult: mockShadowId}
+ }
+ } else {
+ setEncryptor = func(srv *SrvShare) {
+ srv.Encryptor = &stubEncryptor{MockResult: mockPublicId}
+ }
+ }
+
+ return InitSrvShare(config, setIndex, setFs, setEncryptor, setLog, setErr)
+}
+
+func TestList(t *testing.T) {
+ conf := cfg.NewConfig()
+ conf.Production = false
+
+ type Output struct {
+ IndexMap map[string]*fileidx.FileInfo
+ }
+ type TestCase struct {
+ Desc string
+ Output
+ }
+
+ testCases := []TestCase{
+ TestCase{
+ Desc: "success",
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ },
+ "1": &fileidx.FileInfo{
+ Id: "1",
+ },
+ },
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ srv := initServiceForFileInfoTest(conf, testCase.Output.IndexMap, true, []*fileidx.FileInfo{})
+ response := srv.list()
+ resInfos := response.(*ResInfos)
+
+ for _, info := range resInfos.List {
+ infoFromSrv, found := srv.Index.Get(info.Id)
+ if !found || infoFromSrv.Id != info.Id {
+ t.Fatalf("list: file infos are not identical")
+ }
+ }
+
+ if len(resInfos.List) != len(srv.Index.List()) {
+ t.Fatalf("list: file infos are not identical")
+ }
+ }
+}
+
+func TestDel(t *testing.T) {
+ conf := cfg.NewConfig()
+ conf.Production = false
+
+ type Init struct {
+ IndexMap map[string]*fileidx.FileInfo
+ }
+ type Input struct {
+ ShareId string
+ }
+ type Output struct {
+ IndexMap map[string]*fileidx.FileInfo
+ Response httputil.MsgRes
+ }
+ type TestCase struct {
+ Desc string
+ Init
+ Input
+ Output
+ }
+
+ testCases := []TestCase{
+ TestCase{
+ Desc: "success",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ },
+ "1": &fileidx.FileInfo{
+ Id: "1",
+ },
+ },
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "1": &fileidx.FileInfo{
+ Id: "1",
+ },
+ },
+ Response: httputil.Ok200,
+ },
+ },
+ TestCase{
+ Desc: "not found",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "1": &fileidx.FileInfo{
+ Id: "1",
+ },
+ },
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "1": &fileidx.FileInfo{
+ Id: "1",
+ },
+ },
+ Response: httputil.Err404,
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ srv := initServiceForFileInfoTest(conf, testCase.Init.IndexMap, true, []*fileidx.FileInfo{})
+ response := srv.del(testCase.ShareId)
+ res := response.(httputil.MsgRes)
+
+ if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
+ t.Fatalf("del: index incorrect")
+ }
+
+ if res != testCase.Output.Response {
+ t.Fatalf("del: response incorrect got: %v, want: %v", res, testCase.Output.Response)
+ }
+ }
+}
+
+func TestShadowId(t *testing.T) {
+ conf := cfg.NewConfig()
+ conf.Production = false
+
+ type Init struct {
+ IndexMap map[string]*fileidx.FileInfo
+ }
+ type Input struct {
+ ShareId string
+ }
+ type Output struct {
+ IndexMap map[string]*fileidx.FileInfo
+ Response interface{}
+ }
+ type TestCase struct {
+ Desc string
+ Init
+ Input
+ Output
+ }
+
+ testCases := []TestCase{
+ TestCase{
+ Desc: "success",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ },
+ },
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ mockShadowId: &fileidx.FileInfo{
+ Id: mockShadowId,
+ },
+ },
+ Response: &ShareInfo{
+ ShareId: mockShadowId,
+ },
+ },
+ },
+ TestCase{
+ Desc: "original id not exists",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ Response: httputil.Err404,
+ },
+ },
+ TestCase{
+ Desc: "dest id exists",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ },
+ mockShadowId: &fileidx.FileInfo{
+ Id: mockShadowId,
+ },
+ },
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ },
+ mockShadowId: &fileidx.FileInfo{
+ Id: mockShadowId,
+ },
+ },
+ Response: httputil.Err412,
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ srv := initServiceForFileInfoTest(conf, testCase.Init.IndexMap, true, []*fileidx.FileInfo{})
+ response := srv.shadowId(testCase.ShareId)
+
+ switch response.(type) {
+ case *ShareInfo:
+ res := response.(*ShareInfo)
+
+ if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
+ info, found := srv.Index.Get(mockShadowId)
+ t.Fatalf(
+ "shadowId: index incorrect got %v found: %v want %v",
+ info,
+ found,
+ testCase.Output.IndexMap[mockShadowId],
+ )
+ }
+
+ if res.ShareId != mockShadowId {
+ t.Fatalf("shadowId: mockId incorrect")
+ }
+
+ case httputil.MsgRes:
+ res := response.(httputil.MsgRes)
+
+ if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
+ t.Fatalf("shadowId: map not identical")
+ }
+
+ if res != testCase.Output.Response {
+ t.Fatalf("shadowId: response incorrect")
+ }
+ default:
+ t.Fatalf("shadowId: return type not found")
+ }
+ }
+}
+
+func TestPublishId(t *testing.T) {
+ conf := cfg.NewConfig()
+ conf.Production = false
+
+ type Init struct {
+ IndexMap map[string]*fileidx.FileInfo
+ }
+ type Input struct {
+ ShareId string
+ }
+ type Output struct {
+ IndexMap map[string]*fileidx.FileInfo
+ Response interface{}
+ }
+ type TestCase struct {
+ Desc string
+ Init
+ Input
+ Output
+ }
+
+ testCases := []TestCase{
+ TestCase{
+ Desc: "success",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ mockShadowId: &fileidx.FileInfo{
+ Id: mockShadowId,
+ },
+ },
+ },
+ Input: Input{
+ ShareId: mockShadowId,
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ mockPublicId: &fileidx.FileInfo{
+ Id: mockPublicId,
+ },
+ },
+ Response: &ShareInfo{
+ ShareId: mockPublicId,
+ },
+ },
+ },
+ TestCase{
+ Desc: "original id not exists",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ Response: httputil.Err404,
+ },
+ },
+ TestCase{
+ Desc: "dest id exists",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ mockShadowId: &fileidx.FileInfo{
+ Id: mockShadowId,
+ },
+ mockPublicId: &fileidx.FileInfo{
+ Id: mockPublicId,
+ },
+ },
+ },
+ Input: Input{
+ ShareId: mockShadowId,
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ mockShadowId: &fileidx.FileInfo{
+ Id: mockShadowId,
+ },
+ mockPublicId: &fileidx.FileInfo{
+ Id: mockPublicId,
+ },
+ },
+ Response: httputil.Err412,
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ srv := initServiceForFileInfoTest(conf, testCase.Init.IndexMap, false, []*fileidx.FileInfo{})
+ response := srv.publishId(testCase.ShareId)
+
+ switch response.(type) {
+ case *ShareInfo:
+ res := response.(*ShareInfo)
+
+ if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
+ info, found := srv.Index.Get(mockPublicId)
+ t.Fatalf(
+ "shadowId: index incorrect got %v found: %v want %v",
+ info,
+ found,
+ testCase.Output.IndexMap[mockPublicId],
+ )
+ }
+
+ if res.ShareId != mockPublicId {
+ t.Fatalf("shadowId: mockId incorrect", res.ShareId, mockPublicId)
+ }
+
+ case httputil.MsgRes:
+ res := response.(httputil.MsgRes)
+
+ if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
+ t.Fatalf("shadowId: map not identical")
+ }
+
+ if res != testCase.Output.Response {
+ t.Fatalf("shadowId: response incorrect got: %v want: %v", res, testCase.Output.Response)
+ }
+ default:
+ t.Fatalf("shadowId: return type not found")
+ }
+ }
+}
+
+func TestSetDownLimit(t *testing.T) {
+ conf := cfg.NewConfig()
+ conf.Production = false
+ mockDownLimit := 100
+
+ type Init struct {
+ IndexMap map[string]*fileidx.FileInfo
+ }
+ type Input struct {
+ ShareId string
+ DownLimit int
+ }
+ type Output struct {
+ IndexMap map[string]*fileidx.FileInfo
+ Response httputil.MsgRes
+ }
+ type TestCase struct {
+ Desc string
+ Init
+ Input
+ Output
+ }
+
+ testCases := []TestCase{
+ TestCase{
+ Desc: "success",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ },
+ },
+ },
+ Input: Input{
+ ShareId: "0",
+ DownLimit: mockDownLimit,
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ "0": &fileidx.FileInfo{
+ Id: "0",
+ DownLimit: mockDownLimit,
+ },
+ },
+ Response: httputil.Ok200,
+ },
+ },
+ TestCase{
+ Desc: "not found",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ },
+ Input: Input{
+ ShareId: "0",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ Response: httputil.Err404,
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ srv := initServiceForFileInfoTest(conf, testCase.Init.IndexMap, true, []*fileidx.FileInfo{})
+ response := srv.setDownLimit(testCase.ShareId, mockDownLimit)
+ res := response.(httputil.MsgRes)
+
+ if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
+ info, _ := srv.Index.Get(testCase.ShareId)
+ t.Fatalf(
+ "setDownLimit: index incorrect got: %v want: %v",
+ info,
+ testCase.Output.IndexMap[testCase.ShareId],
+ )
+ }
+
+ if res != testCase.Output.Response {
+ t.Fatalf("setDownLimit: response incorrect got: %v, want: %v", res, testCase.Output.Response)
+ }
+ }
+}
+
+func TestAddLocalFiles(t *testing.T) {
+ conf := cfg.NewConfig()
+ conf.Production = false
+
+ type Init struct {
+ Infos []*fileidx.FileInfo
+ }
+ type Output struct {
+ IndexMap map[string]*fileidx.FileInfo
+ Response httputil.MsgRes
+ }
+ type TestCase struct {
+ Desc string
+ Init
+ Output
+ }
+
+ testCases := []TestCase{
+ TestCase{
+ Desc: "success",
+ Init: Init{
+ Infos: []*fileidx.FileInfo{
+ &fileidx.FileInfo{
+ Id: "",
+ DownLimit: 0,
+ ModTime: 13,
+ PathLocal: "filename1",
+ State: "",
+ Uploaded: 13,
+ },
+ },
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ mockPublicId: &fileidx.FileInfo{
+ Id: mockPublicId,
+ DownLimit: conf.DownLimit,
+ ModTime: 13,
+ PathLocal: filepath.Join(conf.PathLocal, "filename1"),
+ State: fileidx.StateDone,
+ Uploaded: 13,
+ },
+ },
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ srv := initServiceForFileInfoTest(conf, testCase.Output.IndexMap, false, testCase.Init.Infos)
+ response := srv.AddLocalFilesImp()
+ res := response.(httputil.MsgRes)
+
+ if res.Code != 200 {
+ t.Fatalf("addLocalFiles: code not correct")
+ }
+
+ if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
+ t.Fatalf(
+ "addLocalFiles: indexes not identical got: %v want: %v",
+ srv.Index.List(),
+ testCase.Output.IndexMap,
+ )
+ }
+ }
+}
diff --git a/server/apis/service.go b/server/apis/service.go
new file mode 100644
index 0000000..9377a88
--- /dev/null
+++ b/server/apis/service.go
@@ -0,0 +1,145 @@
+package apis
+
+import (
+ "log"
+ "net/http"
+ "os"
+ "strings"
+)
+
+import (
+ "quickshare/server/libs/cfg"
+ "quickshare/server/libs/encrypt"
+ "quickshare/server/libs/errutil"
+ "quickshare/server/libs/fileidx"
+ "quickshare/server/libs/fsutil"
+ "quickshare/server/libs/httputil"
+ "quickshare/server/libs/httpworker"
+ "quickshare/server/libs/limiter"
+ "quickshare/server/libs/logutil"
+ "quickshare/server/libs/qtube"
+ "quickshare/server/libs/walls"
+)
+
+type AddDep func(*SrvShare)
+
+func NewSrvShare(config *cfg.Config) *SrvShare {
+ logger := logutil.NewSlog(os.Stdout, config.AppName)
+ setLog := func(srv *SrvShare) {
+ srv.Log = logger
+ }
+
+ errChecker := errutil.NewErrChecker(!config.Production, logger)
+ setErr := func(srv *SrvShare) {
+ srv.Err = errChecker
+ }
+
+ setWorkerPool := func(srv *SrvShare) {
+ workerPoolSize := config.WorkerPoolSize
+ taskQueueSize := config.TaskQueueSize
+ srv.WorkerPool = httpworker.NewWorkerPool(workerPoolSize, taskQueueSize, logger)
+ }
+
+ setWalls := func(srv *SrvShare) {
+ encrypterMaker := encrypt.JwtEncrypterMaker
+ ipLimiter := limiter.NewRateLimiter(
+ config.LimiterCap,
+ config.LimiterTtl,
+ config.LimiterCyc,
+ config.BucketCap,
+ config.SpecialCaps,
+ )
+ opLimiter := limiter.NewRateLimiter(
+ config.LimiterCap,
+ config.LimiterTtl,
+ config.LimiterCyc,
+ config.BucketCap,
+ config.SpecialCaps,
+ )
+ srv.Walls = walls.NewAccessWalls(config, ipLimiter, opLimiter, encrypterMaker)
+ }
+
+ setIndex := func(srv *SrvShare) {
+ srv.Index = fileidx.NewMemFileIndex(config.MaxShares)
+ }
+
+ fs := fsutil.NewSimpleFs(errChecker)
+ setFs := func(srv *SrvShare) {
+ srv.Fs = fs
+ }
+
+ setDownloader := func(srv *SrvShare) {
+ srv.Downloader = qtube.NewQTube(
+ config.PathLocal,
+ config.MaxDownBytesPerSec,
+ config.MaxRangeLength,
+ fs,
+ )
+ }
+
+ setEncryptor := func(srv *SrvShare) {
+ srv.Encryptor = &encrypt.HmacEncryptor{Key: config.SecretKeyByte}
+ }
+
+ setHttp := func(srv *SrvShare) {
+ srv.Http = &httputil.QHttpUtil{
+ CookieDomain: config.CookieDomain,
+ CookieHttpOnly: config.CookieHttpOnly,
+ CookieMaxAge: config.CookieMaxAge,
+ CookiePath: config.CookiePath,
+ CookieSecure: config.CookieSecure,
+ Err: errChecker,
+ }
+ }
+
+ return InitSrvShare(config, setIndex, setWalls, setWorkerPool, setFs, setDownloader, setEncryptor, setLog, setErr, setHttp)
+}
+
+func InitSrvShare(config *cfg.Config, addDeps ...AddDep) *SrvShare {
+ srv := &SrvShare{}
+ srv.Conf = config
+ for _, addDep := range addDeps {
+ addDep(srv)
+ }
+
+ if !srv.Fs.MkdirAll(srv.Conf.PathLocal, os.FileMode(0775)) {
+ panic("fail to make ./files/ folder")
+ }
+
+ if res := srv.AddLocalFilesImp(); res != httputil.Ok200 {
+ panic("fail to add local files")
+ }
+
+ return srv
+}
+
+type SrvShare struct {
+ Conf *cfg.Config
+ Encryptor encrypt.Encryptor
+ Err errutil.ErrUtil
+ Downloader qtube.Downloader
+ Http httputil.HttpUtil
+ Index fileidx.FileIndex
+ Fs fsutil.FsUtil
+ Log logutil.LogUtil
+ Walls walls.Walls
+ WorkerPool httpworker.Workers
+}
+
+func (srv *SrvShare) Wrap(serviceFunc httpworker.ServiceFunc) httpworker.DoFunc {
+ return func(res http.ResponseWriter, req *http.Request) {
+ body := serviceFunc(res, req)
+
+ if body != nil && body != 0 && srv.Http.Fill(body, res) <= 0 {
+ log.Println("Wrap: fail to fill body", body, res)
+ }
+ }
+}
+
+func GetRemoteIp(addr string) string {
+ addrParts := strings.Split(addr, ":")
+ if len(addrParts) > 0 {
+ return addrParts[0]
+ }
+ return "unknown ip"
+}
diff --git a/server/apis/test_helper.go b/server/apis/test_helper.go
new file mode 100644
index 0000000..3d4d5de
--- /dev/null
+++ b/server/apis/test_helper.go
@@ -0,0 +1,117 @@
+package apis
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+)
+
+import (
+ "quickshare/server/libs/fileidx"
+ "quickshare/server/libs/qtube"
+)
+
+type stubFsUtil struct {
+ MockLocalFileInfos []*fileidx.FileInfo
+ MockFile *qtube.StubFile
+}
+
+var expectCreateFileName = ""
+
+func (fs *stubFsUtil) CreateFile(fileName string) error {
+ if fileName != expectCreateFileName {
+ panic(
+ fmt.Sprintf("CreateFile: got: %s expect: %s", fileName, expectCreateFileName),
+ )
+ }
+ return nil
+}
+
+func (fs *stubFsUtil) CopyChunkN(fullPath string, chunk io.Reader, start int64, len int64) bool {
+ return true
+}
+
+func (fs *stubFsUtil) ServeFile(res http.ResponseWriter, req *http.Request, fileName string) {
+ return
+}
+
+func (fs *stubFsUtil) DelFile(fullPath string) bool {
+ return true
+}
+
+func (fs *stubFsUtil) MkdirAll(path string, mode os.FileMode) bool {
+ return true
+}
+
+func (fs *stubFsUtil) Readdir(dirname string, n int) ([]*fileidx.FileInfo, error) {
+ return fs.MockLocalFileInfos, nil
+}
+
+func (fs *stubFsUtil) Open(filePath string) (qtube.ReadSeekCloser, error) {
+ return fs.MockFile, nil
+}
+
+type stubWriter struct {
+ Headers http.Header
+ Response []byte
+ StatusCode int
+}
+
+func (w *stubWriter) Header() http.Header {
+ return w.Headers
+}
+
+func (w *stubWriter) Write(body []byte) (int, error) {
+ w.Response = append(w.Response, body...)
+ return len(body), nil
+}
+
+func (w *stubWriter) WriteHeader(statusCode int) {
+ w.StatusCode = statusCode
+}
+
+type stubDownloader struct {
+ Content string
+}
+
+func (d stubDownloader) ServeFile(w http.ResponseWriter, r *http.Request, fileInfo *fileidx.FileInfo) error {
+ _, err := w.Write([]byte(d.Content))
+ return err
+}
+
+func sameInfoWithoutTime(info1, info2 *fileidx.FileInfo) bool {
+ return info1.Id == info2.Id &&
+ info1.DownLimit == info2.DownLimit &&
+ info1.PathLocal == info2.PathLocal &&
+ info1.State == info2.State &&
+ info1.Uploaded == info2.Uploaded
+}
+
+func sameMap(map1, map2 map[string]*fileidx.FileInfo) bool {
+ for key, info1 := range map1 {
+ info2, found := map2[key]
+ if !found || !sameInfoWithoutTime(info1, info2) {
+ fmt.Printf("infos are not same: \n%v \n%v", info1, info2)
+ return false
+ }
+ }
+
+ for key, info2 := range map2 {
+ info1, found := map1[key]
+ if !found || !sameInfoWithoutTime(info1, info2) {
+ fmt.Printf("infos are not same: \n%v \n%v", info1, info2)
+ return false
+ }
+ }
+
+ return true
+}
+
+type stubEncryptor struct {
+ MockResult string
+}
+
+func (enc *stubEncryptor) Encrypt(content []byte) string {
+ return enc.MockResult
+}
diff --git a/server/apis/upload.go b/server/apis/upload.go
new file mode 100644
index 0000000..2cd2a78
--- /dev/null
+++ b/server/apis/upload.go
@@ -0,0 +1,253 @@
+package apis
+
+import (
+ "io"
+ "net/http"
+ "path/filepath"
+ "strconv"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/encrypt"
+ "quickshare/server/libs/fileidx"
+ "quickshare/server/libs/fsutil"
+ httpUtil "quickshare/server/libs/httputil"
+ worker "quickshare/server/libs/httpworker"
+)
+
+const DefaultId = "0"
+
+type ByteRange struct {
+ ShareId string
+ Start int64
+ Length int64
+}
+
+type ShareInfo struct {
+ ShareId string
+}
+
+func (srv *SrvShare) StartUploadHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodPost {
+ srv.Http.Fill(httpUtil.Err404, res)
+ return
+ }
+
+ tokenStr := srv.Http.GetCookie(req.Cookies(), srv.Conf.KeyToken)
+ ipPass := srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr))
+ loginPass := srv.Walls.PassLoginCheck(tokenStr, req)
+ opPass := srv.Walls.PassOpLimit(GetRemoteIp(req.RemoteAddr), srv.Conf.OpIdUpload)
+ if !ipPass || !loginPass || !opPass {
+ srv.Http.Fill(httpUtil.Err429, res)
+ return
+ }
+
+ ack := make(chan error, 1)
+ ok := srv.WorkerPool.Put(&worker.Task{
+ Ack: ack,
+ Do: srv.Wrap(srv.StartUpload),
+ Res: res,
+ Req: req,
+ })
+ if !ok {
+ srv.Http.Fill(httpUtil.Err503, res)
+ }
+
+ execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
+ if srv.Err.IsErr(execErr) {
+ srv.Http.Fill(httpUtil.Err500, res)
+ }
+}
+
+func (srv *SrvShare) UploadHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodPost {
+ srv.Http.Fill(httpUtil.Err404, res)
+ return
+ }
+
+ tokenStr := srv.Http.GetCookie(req.Cookies(), srv.Conf.KeyToken)
+ ipPass := srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr))
+ loginPass := srv.Walls.PassLoginCheck(tokenStr, req)
+ opPass := srv.Walls.PassOpLimit(GetRemoteIp(req.RemoteAddr), srv.Conf.OpIdUpload)
+ if !ipPass || !loginPass || !opPass {
+ srv.Http.Fill(httpUtil.Err429, res)
+ return
+ }
+
+ multiFormErr := req.ParseMultipartForm(srv.Conf.ParseFormBufSize)
+ if srv.Err.IsErr(multiFormErr) {
+ srv.Http.Fill(httpUtil.Err400, res)
+ return
+ }
+
+ srv.Log.Println("form", req.Form)
+ srv.Log.Println("pform", req.PostForm)
+ srv.Log.Println("mform", req.MultipartForm)
+ ack := make(chan error, 1)
+ ok := srv.WorkerPool.Put(&worker.Task{
+ Ack: ack,
+ Do: srv.Wrap(srv.Upload),
+ Res: res,
+ Req: req,
+ })
+ if !ok {
+ srv.Http.Fill(httpUtil.Err503, res)
+ }
+
+ execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
+ if srv.Err.IsErr(execErr) {
+ srv.Http.Fill(httpUtil.Err500, res)
+ }
+}
+
+func (srv *SrvShare) FinishUploadHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodPost {
+ srv.Http.Fill(httpUtil.Err404, res)
+ return
+ }
+
+ tokenStr := srv.Http.GetCookie(req.Cookies(), srv.Conf.KeyToken)
+ ipPass := srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr))
+ loginPass := srv.Walls.PassLoginCheck(tokenStr, req)
+ opPass := srv.Walls.PassOpLimit(GetRemoteIp(req.RemoteAddr), srv.Conf.OpIdUpload)
+ if !ipPass || !loginPass || !opPass {
+ srv.Http.Fill(httpUtil.Err429, res)
+ return
+ }
+
+ ack := make(chan error, 1)
+ ok := srv.WorkerPool.Put(&worker.Task{
+ Ack: ack,
+ Do: srv.Wrap(srv.FinishUpload),
+ Res: res,
+ Req: req,
+ })
+ if !ok {
+ srv.Http.Fill(httpUtil.Err503, res)
+ }
+
+ execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
+ if srv.Err.IsErr(execErr) {
+ srv.Http.Fill(httpUtil.Err500, res)
+ }
+}
+
+func (srv *SrvShare) StartUpload(res http.ResponseWriter, req *http.Request) interface{} {
+ return srv.startUpload(req.FormValue(srv.Conf.KeyFileName))
+}
+
+func (srv *SrvShare) startUpload(fileName string) interface{} {
+ if !IsValidFileName(fileName) {
+ return httpUtil.Err400
+ }
+
+ id := DefaultId
+ if srv.Conf.Production {
+ id = genInfoId(fileName, srv.Conf.SecretKeyByte)
+ }
+
+ info := &fileidx.FileInfo{
+ Id: id,
+ DownLimit: srv.Conf.DownLimit,
+ ModTime: time.Now().UnixNano(),
+ PathLocal: fileName,
+ Uploaded: 0,
+ State: fileidx.StateStarted,
+ }
+
+ switch srv.Index.Add(info) {
+ case 0:
+ // go on
+ case -1:
+ return httpUtil.Err412
+ case 1:
+ return httpUtil.Err500 // TODO: use correct status code
+ default:
+ srv.Index.Del(id)
+ return httpUtil.Err500
+ }
+
+ fullPath := filepath.Join(srv.Conf.PathLocal, info.PathLocal)
+ createFileErr := srv.Fs.CreateFile(fullPath)
+ switch {
+ case createFileErr == fsutil.ErrExists:
+ srv.Index.Del(id)
+ return httpUtil.Err412
+ case createFileErr == fsutil.ErrUnknown:
+ srv.Index.Del(id)
+ return httpUtil.Err500
+ default:
+ srv.Index.SetState(id, fileidx.StateUploading)
+ return &ByteRange{
+ ShareId: id,
+ Start: 0,
+ Length: srv.Conf.MaxUpBytesPerSec,
+ }
+ }
+}
+
+func (srv *SrvShare) Upload(res http.ResponseWriter, req *http.Request) interface{} {
+ shareId := req.FormValue(srv.Conf.KeyShareId)
+ start, startErr := strconv.ParseInt(req.FormValue(srv.Conf.KeyStart), 10, 64)
+ length, lengthErr := strconv.ParseInt(req.FormValue(srv.Conf.KeyLen), 10, 64)
+ chunk, _, chunkErr := req.FormFile(srv.Conf.KeyChunk)
+
+ if srv.Err.IsErr(startErr) ||
+ srv.Err.IsErr(lengthErr) ||
+ srv.Err.IsErr(chunkErr) {
+ return httpUtil.Err400
+ }
+
+ return srv.upload(shareId, start, length, chunk)
+}
+
+func (srv *SrvShare) upload(shareId string, start int64, length int64, chunk io.Reader) interface{} {
+ if !srv.IsValidShareId(shareId) {
+ return httpUtil.Err400
+ }
+
+ fileInfo, found := srv.Index.Get(shareId)
+ if !found {
+ return httpUtil.Err404
+ }
+
+ if !srv.IsValidStart(start, fileInfo.Uploaded) || !srv.IsValidLength(length) {
+ return httpUtil.Err400
+ }
+
+ fullPath := filepath.Join(srv.Conf.PathLocal, fileInfo.PathLocal)
+ if !srv.Fs.CopyChunkN(fullPath, chunk, start, length) {
+ return httpUtil.Err500
+ }
+
+ if srv.Index.IncrUploaded(shareId, length) == 0 {
+ return httpUtil.Err404
+ }
+
+ return &ByteRange{
+ ShareId: shareId,
+ Start: start + length,
+ Length: srv.Conf.MaxUpBytesPerSec,
+ }
+}
+
+func (srv *SrvShare) FinishUpload(res http.ResponseWriter, req *http.Request) interface{} {
+ shareId := req.FormValue(srv.Conf.KeyShareId)
+ return srv.finishUpload(shareId)
+}
+
+func (srv *SrvShare) finishUpload(shareId string) interface{} {
+ if !srv.Index.SetState(shareId, fileidx.StateDone) {
+ return httpUtil.Err404
+ }
+
+ return &ShareInfo{
+ ShareId: shareId,
+ }
+}
+
+func genInfoId(content string, key []byte) string {
+ encrypter := encrypt.HmacEncryptor{Key: key}
+ return encrypter.Encrypt([]byte(content))
+}
diff --git a/server/apis/upload_test.go b/server/apis/upload_test.go
new file mode 100644
index 0000000..57c1e95
--- /dev/null
+++ b/server/apis/upload_test.go
@@ -0,0 +1,368 @@
+package apis
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/cfg"
+ "quickshare/server/libs/encrypt"
+ "quickshare/server/libs/errutil"
+ "quickshare/server/libs/fileidx"
+ "quickshare/server/libs/httputil"
+ "quickshare/server/libs/httpworker"
+ "quickshare/server/libs/limiter"
+ "quickshare/server/libs/logutil"
+ "quickshare/server/libs/walls"
+)
+
+const testCap = 3
+
+func initServiceForUploadTest(config *cfg.Config, indexMap map[string]*fileidx.FileInfo) *SrvShare {
+ logger := logutil.NewSlog(os.Stdout, config.AppName)
+ setLog := func(srv *SrvShare) {
+ srv.Log = logger
+ }
+
+ setWorkerPool := func(srv *SrvShare) {
+ workerPoolSize := config.WorkerPoolSize
+ taskQueueSize := config.TaskQueueSize
+ srv.WorkerPool = httpworker.NewWorkerPool(workerPoolSize, taskQueueSize, logger)
+ }
+
+ setWalls := func(srv *SrvShare) {
+ encrypterMaker := encrypt.JwtEncrypterMaker
+ ipLimiter := limiter.NewRateLimiter(config.LimiterCap, config.LimiterTtl, config.LimiterCyc, config.BucketCap, map[int16]int16{})
+ opLimiter := limiter.NewRateLimiter(config.LimiterCap, config.LimiterTtl, config.LimiterCyc, config.BucketCap, map[int16]int16{})
+ srv.Walls = walls.NewAccessWalls(config, ipLimiter, opLimiter, encrypterMaker)
+ }
+
+ setIndex := func(srv *SrvShare) {
+ srv.Index = fileidx.NewMemFileIndexWithMap(len(indexMap)+testCap, indexMap)
+ }
+
+ setFs := func(srv *SrvShare) {
+ srv.Fs = &stubFsUtil{}
+ }
+
+ setEncryptor := func(srv *SrvShare) {
+ srv.Encryptor = &encrypt.HmacEncryptor{Key: config.SecretKeyByte}
+ }
+
+ errChecker := errutil.NewErrChecker(!config.Production, logger)
+ setErr := func(srv *SrvShare) {
+ srv.Err = errChecker
+ }
+
+ return InitSrvShare(config, setIndex, setWalls, setWorkerPool, setFs, setEncryptor, setLog, setErr)
+}
+
+func TestStartUpload(t *testing.T) {
+ conf := cfg.NewConfig()
+ conf.Production = false
+
+ type Init struct {
+ IndexMap map[string]*fileidx.FileInfo
+ }
+ type Input struct {
+ FileName string
+ }
+ type Output struct {
+ Response interface{}
+ IndexMap map[string]*fileidx.FileInfo
+ }
+ type testCase struct {
+ Desc string
+ Init
+ Input
+ Output
+ }
+
+ testCases := []testCase{
+ testCase{
+ Desc: "invalid file name",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ },
+ Input: Input{
+ FileName: "",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ Response: httputil.Err400,
+ },
+ },
+ testCase{
+ Desc: "succeed to start uploading",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ },
+ Input: Input{
+ FileName: "filename",
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ DefaultId: &fileidx.FileInfo{
+ Id: DefaultId,
+ DownLimit: conf.DownLimit,
+ ModTime: time.Now().UnixNano(),
+ PathLocal: "filename",
+ Uploaded: 0,
+ State: fileidx.StateUploading,
+ },
+ },
+ Response: &ByteRange{
+ ShareId: DefaultId,
+ Start: 0,
+ Length: conf.MaxUpBytesPerSec,
+ },
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ srv := initServiceForUploadTest(conf, testCase.Init.IndexMap)
+
+ // verify CreateFile
+ expectCreateFileName = filepath.Join(conf.PathLocal, testCase.FileName)
+
+ response := srv.startUpload(testCase.FileName)
+
+ // verify index
+ if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
+ t.Fatalf("startUpload: index not equal got: %v, %v, expect: %v", srv.Index.List(), response, testCase.Output.IndexMap)
+ }
+
+ // verify response
+ switch expectRes := testCase.Output.Response.(type) {
+ case *ByteRange:
+ res := response.(*ByteRange)
+ if res.ShareId != expectRes.ShareId ||
+ res.Start != expectRes.Start ||
+ res.Length != expectRes.Length {
+ t.Fatalf(fmt.Sprintf("startUpload: res=%v expect=%v", res, expectRes))
+ }
+ case httputil.MsgRes:
+ if response != expectRes {
+ t.Fatalf(fmt.Sprintf("startUpload: reponse=%v expectRes=%v", response, expectRes))
+ }
+ default:
+ t.Fatalf(fmt.Sprintf("startUpload: type not found: %T %T", testCase.Output.Response, httputil.Err400))
+ }
+ }
+}
+
+func TestUpload(t *testing.T) {
+ conf := cfg.NewConfig()
+ conf.Production = false
+
+ type Init struct {
+ IndexMap map[string]*fileidx.FileInfo
+ }
+ type Input struct {
+ ShareId string
+ Start int64
+ Len int64
+ Chunk io.Reader
+ }
+ type Output struct {
+ IndexMap map[string]*fileidx.FileInfo
+ Response interface{}
+ }
+ type testCase struct {
+ Desc string
+ Init
+ Input
+ Output
+ }
+
+ testCases := []testCase{
+ testCase{
+ Desc: "shareid does not exist",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ },
+ Input: Input{
+ ShareId: DefaultId,
+ Start: 0,
+ Len: 1,
+ Chunk: strings.NewReader(""),
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ Response: httputil.Err404,
+ },
+ },
+ testCase{
+ Desc: "succeed",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ DefaultId: &fileidx.FileInfo{
+ Id: DefaultId,
+ DownLimit: conf.MaxShares,
+ PathLocal: "path/filename",
+ State: fileidx.StateUploading,
+ Uploaded: 0,
+ },
+ },
+ },
+ Input: Input{
+ ShareId: DefaultId,
+ Start: 0,
+ Len: 1,
+ Chunk: strings.NewReader("a"),
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ DefaultId: &fileidx.FileInfo{
+ Id: DefaultId,
+ DownLimit: conf.MaxShares,
+ PathLocal: "path/filename",
+ State: fileidx.StateUploading,
+ Uploaded: 1,
+ },
+ },
+ Response: &ByteRange{
+ ShareId: DefaultId,
+ Start: 1,
+ Length: conf.MaxUpBytesPerSec,
+ },
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ srv := initServiceForUploadTest(conf, testCase.Init.IndexMap)
+
+ response := srv.upload(
+ testCase.Input.ShareId,
+ testCase.Input.Start,
+ testCase.Input.Len,
+ testCase.Input.Chunk,
+ )
+
+ // TODO: not verified copyChunk
+
+ // verify index
+ if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
+ t.Fatalf("upload: index not identical got: %v want: %v", srv.Index.List(), testCase.Output.IndexMap)
+ }
+ // verify response
+ switch response.(type) {
+ case *ByteRange:
+ br := testCase.Output.Response.(*ByteRange)
+ res := response.(*ByteRange)
+ if res.ShareId != br.ShareId || res.Start != br.Start || res.Length != br.Length {
+ t.Fatalf(fmt.Sprintf("upload: response=%v expectRes=%v", res, br))
+ }
+ default:
+ if response != testCase.Output.Response {
+ t.Fatalf(fmt.Sprintf("upload: response=%v expectRes=%v", response, testCase.Output.Response))
+ }
+ }
+ }
+}
+
+func TestFinishUpload(t *testing.T) {
+ conf := cfg.NewConfig()
+ conf.Production = false
+
+ type Init struct {
+ IndexMap map[string]*fileidx.FileInfo
+ }
+ type Input struct {
+ ShareId string
+ Start int64
+ Len int64
+ Chunk io.Reader
+ }
+ type Output struct {
+ IndexMap map[string]*fileidx.FileInfo
+ Response interface{}
+ }
+ type testCase struct {
+ Desc string
+ Init
+ Input
+ Output
+ }
+
+ testCases := []testCase{
+ testCase{
+ Desc: "success",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{
+ DefaultId: &fileidx.FileInfo{
+ Id: DefaultId,
+ DownLimit: conf.MaxShares,
+ PathLocal: "path/filename",
+ State: fileidx.StateUploading,
+ Uploaded: 1,
+ },
+ },
+ },
+ Input: Input{
+ ShareId: DefaultId,
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{
+ DefaultId: &fileidx.FileInfo{
+ Id: DefaultId,
+ DownLimit: conf.MaxShares,
+ PathLocal: "path/filename",
+ State: fileidx.StateDone,
+ Uploaded: 1,
+ },
+ },
+ Response: &ShareInfo{
+ ShareId: DefaultId,
+ },
+ },
+ },
+ testCase{
+ Desc: "shareId exists",
+ Init: Init{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ },
+ Input: Input{
+ ShareId: DefaultId,
+ },
+ Output: Output{
+ IndexMap: map[string]*fileidx.FileInfo{},
+ Response: httputil.Err404,
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ srv := initServiceForUploadTest(conf, testCase.Init.IndexMap)
+
+ response := srv.finishUpload(testCase.ShareId)
+
+ if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
+ t.Fatalf("finishUpload: index not identical got: %v, want: %v", srv.Index.List(), testCase.Output.IndexMap)
+ }
+
+ switch res := response.(type) {
+ case httputil.MsgRes:
+ expectRes := testCase.Output.Response.(httputil.MsgRes)
+ if res != expectRes {
+ t.Fatalf(fmt.Sprintf("finishUpload: reponse=%v expectRes=%v", res, expectRes))
+ }
+ case *ShareInfo:
+ info, found := testCase.Output.IndexMap[res.ShareId]
+ if !found || info.State != fileidx.StateDone {
+ // TODO: should use isValidUrl or better to verify result
+ t.Fatalf(fmt.Sprintf("finishUpload: share info is not correct: received: %v expect: %v", res.ShareId, testCase.ShareId))
+ }
+ default:
+ t.Fatalf(fmt.Sprintf("finishUpload: type not found: %T %T", response, testCase.Output.Response))
+ }
+ }
+}
diff --git a/server/libs/cfg/cfg.go b/server/libs/cfg/cfg.go
new file mode 100644
index 0000000..c1272de
--- /dev/null
+++ b/server/libs/cfg/cfg.go
@@ -0,0 +1,251 @@
+package cfg
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "strconv"
+ "strings"
+)
+
+type Config struct {
+ AppName string
+ AdminId string
+ AdminPwd string
+ SecretKey string
+ SecretKeyByte []byte `json:",omitempty"`
+ // server
+ Production bool
+ HostName string
+ Port int
+ // performance
+ MaxUpBytesPerSec int64
+ MaxDownBytesPerSec int64
+ MaxRangeLength int64
+ Timeout int // millisecond
+ ReadTimeout int
+ WriteTimeout int
+ IdleTimeout int
+ WorkerPoolSize int
+ TaskQueueSize int
+ QueueSize int
+ ParseFormBufSize int64
+ MaxHeaderBytes int
+ DownLimit int
+ MaxShares int
+ LocalFileLimit int
+ // Cookie
+ CookieDomain string
+ CookieHttpOnly bool
+ CookieMaxAge int
+ CookiePath string
+ CookieSecure bool
+ // keys
+ KeyAdminId string
+ KeyAdminPwd string
+ KeyToken string
+ KeyFileName string
+ KeyFileSize string
+ KeyShareId string
+ KeyStart string
+ KeyLen string
+ KeyChunk string
+ KeyAct string
+ KeyExpires string
+ KeyDownLimit string
+ ActStartUpload string
+ ActUpload string
+ ActFinishUpload string
+ ActLogin string
+ ActLogout string
+ ActShadowId string
+ ActPublishId string
+ ActSetDownLimit string
+ ActAddLocalFiles string
+ // resource id
+ AllUsers string
+ // opIds
+ OpIdIpVisit int16
+ OpIdUpload int16
+ OpIdDownload int16
+ OpIdLogin int16
+ OpIdGetFInfo int16
+ OpIdDelFInfo int16
+ OpIdOpFInfo int16
+ // local
+ PathLocal string
+ PathLogin string
+ PathDownloadLogin string
+ PathDownload string
+ PathUpload string
+ PathStartUpload string
+ PathFinishUpload string
+ PathFileInfo string
+ PathClient string
+ // rate Limiter
+ LimiterCap int64
+ LimiterTtl int32
+ LimiterCyc int32
+ BucketCap int16
+ SpecialCapsStr map[string]int16
+ SpecialCaps map[int16]int16
+}
+
+func NewConfig() *Config {
+ config := &Config{
+ // secrets
+ AppName: "qs",
+ AdminId: "admin",
+ AdminPwd: "qs",
+ SecretKey: "qs",
+ SecretKeyByte: []byte("qs"),
+ // server
+ Production: true,
+ HostName: "localhost",
+ Port: 8888,
+ // performance
+ MaxUpBytesPerSec: 500 * 1000,
+ MaxDownBytesPerSec: 500 * 1000,
+ MaxRangeLength: 10 * 1024 * 1024,
+ Timeout: 500, // millisecond,
+ ReadTimeout: 500,
+ WriteTimeout: 43200000,
+ IdleTimeout: 10000,
+ WorkerPoolSize: 2,
+ TaskQueueSize: 2,
+ QueueSize: 2,
+ ParseFormBufSize: 600,
+ MaxHeaderBytes: 1 << 15, // 32KB
+ DownLimit: -1,
+ MaxShares: 1 << 31,
+ LocalFileLimit: -1,
+ // Cookie
+ CookieDomain: "",
+ CookieHttpOnly: false,
+ CookieMaxAge: 3600 * 24 * 30, // one week,
+ CookiePath: "/",
+ CookieSecure: false,
+ // keys
+ KeyAdminId: "adminid",
+ KeyAdminPwd: "adminpwd",
+ KeyToken: "token",
+ KeyFileName: "fname",
+ KeyFileSize: "size",
+ KeyShareId: "shareid",
+ KeyStart: "start",
+ KeyLen: "len",
+ KeyChunk: "chunk",
+ KeyAct: "act",
+ KeyExpires: "expires",
+ KeyDownLimit: "downlimit",
+ ActStartUpload: "startupload",
+ ActUpload: "upload",
+ ActFinishUpload: "finishupload",
+ ActLogin: "login",
+ ActLogout: "logout",
+ ActShadowId: "shadowid",
+ ActPublishId: "publishid",
+ ActSetDownLimit: "setdownlimit",
+ ActAddLocalFiles: "addlocalfiles",
+ AllUsers: "allusers",
+ // opIds
+ OpIdIpVisit: 0,
+ OpIdUpload: 1,
+ OpIdDownload: 2,
+ OpIdLogin: 3,
+ OpIdGetFInfo: 4,
+ OpIdDelFInfo: 5,
+ OpIdOpFInfo: 6,
+ // local
+ PathLocal: "files",
+ PathLogin: "/login",
+ PathDownloadLogin: "/download-login",
+ PathDownload: "/download",
+ PathUpload: "/upload",
+ PathStartUpload: "/startupload",
+ PathFinishUpload: "/finishupload",
+ PathFileInfo: "/fileinfo",
+ PathClient: "/",
+ // rate Limiter
+ LimiterCap: 256, // how many op supported for each user
+ LimiterTtl: 3600, // second
+ LimiterCyc: 1, // second
+ BucketCap: 3, // how many op can do per LimiterCyc sec
+ SpecialCaps: map[int16]int16{
+ 0: 5, // ip
+ 1: 1, // upload
+ 2: 1, // download
+ 3: 1, // login
+ },
+ }
+
+ return config
+}
+
+func NewConfigFrom(path string) *Config {
+ configBytes, readErr := ioutil.ReadFile(path)
+ if readErr != nil {
+ panic(fmt.Sprintf("config file not found: %s", path))
+ }
+
+ config := &Config{}
+ marshalErr := json.Unmarshal(configBytes, config)
+
+ // TODO: look for a better solution
+ config.SpecialCaps = make(map[int16]int16)
+ for strKey, value := range config.SpecialCapsStr {
+ key, parseKeyErr := strconv.ParseInt(strKey, 10, 16)
+ if parseKeyErr != nil {
+ panic("fail to parse SpecialCapsStr, its type should be map[int16]int16")
+ }
+ config.SpecialCaps[int16(key)] = value
+ }
+
+ if marshalErr != nil {
+ panic("config file format is incorrect")
+ }
+
+ config.SecretKeyByte = []byte(config.SecretKey)
+ if config.HostName == "" {
+ hostName, err := GetLocalAddr()
+ if err != nil {
+ panic(err)
+ }
+ config.HostName = hostName.String()
+ }
+
+ return config
+}
+
+func GetLocalAddr() (net.IP, error) {
+ fmt.Println(`config.HostName is empty(""), choose one IP for listening automatically.`)
+ infs, err := net.Interfaces()
+ if err != nil {
+ panic("fail to get net interfaces")
+ }
+
+ for _, inf := range infs {
+ if inf.Flags&4 != 4 && !strings.Contains(inf.Name, "docker") {
+ addrs, err := inf.Addrs()
+ if err != nil {
+ panic("fail to get addrs of interface")
+ }
+ for _, addr := range addrs {
+ switch v := addr.(type) {
+ case *net.IPAddr:
+ if !strings.Contains(v.IP.String(), ":") {
+ return v.IP, nil
+ }
+ case *net.IPNet:
+ if !strings.Contains(v.IP.String(), ":") {
+ return v.IP, nil
+ }
+ }
+ }
+ }
+ }
+
+ return nil, errors.New("no addr found")
+}
diff --git a/server/libs/encrypt/encrypter_hmac.go b/server/libs/encrypt/encrypter_hmac.go
new file mode 100644
index 0000000..fc2687d
--- /dev/null
+++ b/server/libs/encrypt/encrypter_hmac.go
@@ -0,0 +1,17 @@
+package encrypt
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/hex"
+)
+
+type HmacEncryptor struct {
+ Key []byte
+}
+
+func (encryptor *HmacEncryptor) Encrypt(content []byte) string {
+ mac := hmac.New(sha256.New, encryptor.Key)
+ mac.Write(content)
+ return hex.EncodeToString(mac.Sum(nil))
+}
diff --git a/server/libs/encrypt/encryptor.go b/server/libs/encrypt/encryptor.go
new file mode 100644
index 0000000..e7438c9
--- /dev/null
+++ b/server/libs/encrypt/encryptor.go
@@ -0,0 +1,5 @@
+package encrypt
+
+type Encryptor interface {
+ Encrypt(content []byte) string
+}
diff --git a/server/libs/encrypt/jwt.go b/server/libs/encrypt/jwt.go
new file mode 100644
index 0000000..e54b5cb
--- /dev/null
+++ b/server/libs/encrypt/jwt.go
@@ -0,0 +1,53 @@
+package encrypt
+
+import (
+ "github.com/robbert229/jwt"
+)
+
+func JwtEncrypterMaker(secret string) TokenEncrypter {
+ return &JwtEncrypter{
+ alg: jwt.HmacSha256(secret),
+ claims: jwt.NewClaim(),
+ }
+}
+
+type JwtEncrypter struct {
+ alg jwt.Algorithm
+ claims *jwt.Claims
+}
+
+func (encrypter *JwtEncrypter) Add(key string, value string) bool {
+ encrypter.claims.Set(key, value)
+ return true
+}
+
+func (encrypter *JwtEncrypter) FromStr(token string) bool {
+ claims, err := encrypter.alg.Decode(token)
+ // TODO: should return error or error info will lost
+ if err != nil {
+ return false
+ }
+
+ encrypter.claims = claims
+ return true
+}
+
+func (encrypter *JwtEncrypter) Get(key string) (string, bool) {
+ iValue, err := encrypter.claims.Get(key)
+ // TODO: should return error or error info will lost
+ if err != nil {
+ return "", false
+ }
+
+ return iValue.(string), true
+}
+
+func (encrypter *JwtEncrypter) ToStr() (string, bool) {
+ token, err := encrypter.alg.Encode(encrypter.claims)
+
+ // TODO: should return error or error info will lost
+ if err != nil {
+ return "", false
+ }
+ return token, true
+}
diff --git a/server/libs/encrypt/token_encrypter.go b/server/libs/encrypt/token_encrypter.go
new file mode 100644
index 0000000..d400163
--- /dev/null
+++ b/server/libs/encrypt/token_encrypter.go
@@ -0,0 +1,11 @@
+package encrypt
+
+type EncrypterMaker func(string) TokenEncrypter
+
+// TODO: name should be Encrypter?
+type TokenEncrypter interface {
+ Add(string, string) bool
+ FromStr(string) bool
+ Get(string) (string, bool)
+ ToStr() (string, bool)
+}
diff --git a/server/libs/errutil/ettutil.go b/server/libs/errutil/ettutil.go
new file mode 100644
index 0000000..24c5831
--- /dev/null
+++ b/server/libs/errutil/ettutil.go
@@ -0,0 +1,59 @@
+package errutil
+
+import (
+ "os"
+ "runtime/debug"
+)
+
+import (
+ "quickshare/server/libs/logutil"
+)
+
+type ErrUtil interface {
+ IsErr(err error) bool
+ IsFatalErr(err error) bool
+ RecoverPanic()
+}
+
+func NewErrChecker(logStack bool, logger logutil.LogUtil) ErrUtil {
+ return &ErrChecker{logStack: logStack, log: logger}
+}
+
+type ErrChecker struct {
+ log logutil.LogUtil
+ logStack bool
+}
+
+// IsErr checks if error occurs
+func (e *ErrChecker) IsErr(err error) bool {
+ if err != nil {
+ e.log.Printf("Error:%q\n", err)
+ if e.logStack {
+ e.log.Println(debug.Stack())
+ }
+ return true
+ }
+ return false
+}
+
+// IsFatalPanic should be used with defer
+func (e *ErrChecker) IsFatalErr(fe error) bool {
+ if fe != nil {
+ e.log.Printf("Panic:%q", fe)
+ if e.logStack {
+ e.log.Println(debug.Stack())
+ }
+ os.Exit(1)
+ }
+ return false
+}
+
+// RecoverPanic catchs the panic and logs panic information
+func (e *ErrChecker) RecoverPanic() {
+ if r := recover(); r != nil {
+ e.log.Printf("Recovered:%v", r)
+ if e.logStack {
+ e.log.Println(debug.Stack())
+ }
+ }
+}
diff --git a/server/libs/fileidx/file_idx.go b/server/libs/fileidx/file_idx.go
new file mode 100644
index 0000000..3b96f34
--- /dev/null
+++ b/server/libs/fileidx/file_idx.go
@@ -0,0 +1,177 @@
+package fileidx
+
+import (
+ "sync"
+)
+
+const (
+ // StateStarted = after startUpload before upload
+ StateStarted = "started"
+ // StateUploading =after upload before finishUpload
+ StateUploading = "uploading"
+ // StateDone = after finishedUpload
+ StateDone = "done"
+)
+
+type FileInfo struct {
+ Id string
+ DownLimit int
+ ModTime int64
+ PathLocal string
+ State string
+ Uploaded int64
+}
+
+type FileIndex interface {
+ Add(fileInfo *FileInfo) int
+ Del(id string)
+ SetId(id string, newId string) bool
+ SetDownLimit(id string, downLimit int) bool
+ DecrDownLimit(id string) (int, bool)
+ SetState(id string, state string) bool
+ IncrUploaded(id string, uploaded int64) int64
+ Get(id string) (*FileInfo, bool)
+ List() map[string]*FileInfo
+}
+
+func NewMemFileIndex(cap int) *MemFileIndex {
+ return &MemFileIndex{
+ cap: cap,
+ infos: make(map[string]*FileInfo, 0),
+ }
+}
+
+func NewMemFileIndexWithMap(cap int, infos map[string]*FileInfo) *MemFileIndex {
+ return &MemFileIndex{
+ cap: cap,
+ infos: infos,
+ }
+}
+
+type MemFileIndex struct {
+ cap int
+ infos map[string]*FileInfo
+ mux sync.RWMutex
+}
+
+func (idx *MemFileIndex) Add(fileInfo *FileInfo) int {
+ idx.mux.Lock()
+ defer idx.mux.Unlock()
+
+ if len(idx.infos) >= idx.cap {
+ return 1
+ }
+
+ if _, found := idx.infos[fileInfo.Id]; found {
+ return -1
+ }
+
+ idx.infos[fileInfo.Id] = fileInfo
+ return 0
+}
+
+func (idx *MemFileIndex) Del(id string) {
+ idx.mux.Lock()
+ defer idx.mux.Unlock()
+
+ delete(idx.infos, id)
+}
+
+func (idx *MemFileIndex) SetId(id string, newId string) bool {
+ if id == newId {
+ return true
+ }
+
+ idx.mux.Lock()
+ defer idx.mux.Unlock()
+
+ info, found := idx.infos[id]
+ if !found {
+ return false
+ }
+
+ if _, foundNewId := idx.infos[newId]; foundNewId {
+ return false
+ }
+
+ idx.infos[newId] = info
+ idx.infos[newId].Id = newId
+ delete(idx.infos, id)
+ return true
+}
+
+func (idx *MemFileIndex) SetDownLimit(id string, downLimit int) bool {
+ idx.mux.Lock()
+ defer idx.mux.Unlock()
+
+ info, found := idx.infos[id]
+ if !found {
+ return false
+ }
+
+ info.DownLimit = downLimit
+ return true
+}
+
+func (idx *MemFileIndex) DecrDownLimit(id string) (int, bool) {
+ idx.mux.Lock()
+ defer idx.mux.Unlock()
+
+ info, found := idx.infos[id]
+ if !found || info.State != StateDone {
+ return 0, false
+ }
+
+ if info.DownLimit == 0 {
+ return 1, false
+ }
+
+ if info.DownLimit > 0 {
+ // info.DownLimit means unlimited
+ info.DownLimit = info.DownLimit - 1
+ }
+ return 1, true
+}
+
+func (idx *MemFileIndex) SetState(id string, state string) bool {
+ idx.mux.Lock()
+ defer idx.mux.Unlock()
+
+ info, found := idx.infos[id]
+ if !found {
+ return false
+ }
+
+ info.State = state
+ return true
+}
+
+func (idx *MemFileIndex) IncrUploaded(id string, uploaded int64) int64 {
+ idx.mux.Lock()
+ defer idx.mux.Unlock()
+
+ info, found := idx.infos[id]
+ if !found {
+ return 0
+ }
+
+ info.Uploaded = info.Uploaded + uploaded
+ return info.Uploaded
+}
+
+func (idx *MemFileIndex) Get(id string) (*FileInfo, bool) {
+ idx.mux.RLock()
+ defer idx.mux.RUnlock()
+
+ infos, found := idx.infos[id]
+ return infos, found
+}
+
+func (idx *MemFileIndex) List() map[string]*FileInfo {
+ idx.mux.RLock()
+ defer idx.mux.RUnlock()
+
+ return idx.infos
+}
+
+// TODO: add unit tests
diff --git a/server/libs/fsutil/fsutil.go b/server/libs/fsutil/fsutil.go
new file mode 100644
index 0000000..dfecbc8
--- /dev/null
+++ b/server/libs/fsutil/fsutil.go
@@ -0,0 +1,118 @@
+package fsutil
+
+import (
+ "errors"
+ "io"
+ "os"
+)
+
+import (
+ "quickshare/server/libs/errutil"
+ "quickshare/server/libs/fileidx"
+ "quickshare/server/libs/qtube"
+)
+
+type FsUtil interface {
+ CreateFile(fullPath string) error
+ CopyChunkN(fullPath string, chunk io.Reader, start int64, length int64) bool
+ DelFile(fullPath string) bool
+ Open(fullPath string) (qtube.ReadSeekCloser, error)
+ MkdirAll(path string, mode os.FileMode) bool
+ Readdir(dirName string, n int) ([]*fileidx.FileInfo, error)
+}
+
+func NewSimpleFs(errUtil errutil.ErrUtil) FsUtil {
+ return &SimpleFs{
+ Err: errUtil,
+ }
+}
+
+type SimpleFs struct {
+ Err errutil.ErrUtil
+}
+
+var (
+ ErrExists = errors.New("file exists")
+ ErrUnknown = errors.New("unknown error")
+)
+
+func (sfs *SimpleFs) CreateFile(fullPath string) error {
+ flag := os.O_CREATE | os.O_EXCL | os.O_RDONLY
+ perm := os.FileMode(0644)
+ newFile, err := os.OpenFile(fullPath, flag, perm)
+ defer newFile.Close()
+
+ if err == nil {
+ return nil
+ } else if os.IsExist(err) {
+ return ErrExists
+ } else {
+ return ErrUnknown
+ }
+}
+
+func (sfs *SimpleFs) CopyChunkN(fullPath string, chunk io.Reader, start int64, length int64) bool {
+ flag := os.O_WRONLY
+ perm := os.FileMode(0644)
+ file, openErr := os.OpenFile(fullPath, flag, perm)
+
+ defer file.Close()
+ if sfs.Err.IsErr(openErr) {
+ return false
+ }
+
+ if _, err := file.Seek(start, io.SeekStart); sfs.Err.IsErr(err) {
+ return false
+ }
+
+ if _, err := io.CopyN(file, chunk, length); sfs.Err.IsErr(err) && err != io.EOF {
+ return false
+ }
+
+ return true
+}
+
+func (sfs *SimpleFs) DelFile(fullPath string) bool {
+ return !sfs.Err.IsErr(os.Remove(fullPath))
+}
+
+func (sfs *SimpleFs) MkdirAll(path string, mode os.FileMode) bool {
+ err := os.MkdirAll(path, mode)
+ return !sfs.Err.IsErr(err)
+}
+
+// TODO: not support read from last seek position
+func (sfs *SimpleFs) Readdir(dirName string, n int) ([]*fileidx.FileInfo, error) {
+ dir, openErr := os.Open(dirName)
+ defer dir.Close()
+
+ if sfs.Err.IsErr(openErr) {
+ return []*fileidx.FileInfo{}, openErr
+ }
+
+ osFileInfos, readErr := dir.Readdir(n)
+ if sfs.Err.IsErr(readErr) && readErr != io.EOF {
+ return []*fileidx.FileInfo{}, readErr
+ }
+
+ fileInfos := make([]*fileidx.FileInfo, 0)
+ for _, osFileInfo := range osFileInfos {
+ if osFileInfo.Mode().IsRegular() {
+ fileInfos = append(
+ fileInfos,
+ &fileidx.FileInfo{
+ ModTime: osFileInfo.ModTime().UnixNano(),
+ PathLocal: osFileInfo.Name(),
+ Uploaded: osFileInfo.Size(),
+ },
+ )
+ }
+ }
+
+ return fileInfos, readErr
+}
+
+// the associated file descriptor has mode O_RDONLY as using os.Open
+func (sfs *SimpleFs) Open(fullPath string) (qtube.ReadSeekCloser, error) {
+ return os.Open(fullPath)
+}
diff --git a/server/libs/httputil/httputil.go b/server/libs/httputil/httputil.go
new file mode 100644
index 0000000..06a49df
--- /dev/null
+++ b/server/libs/httputil/httputil.go
@@ -0,0 +1,84 @@
+package httputil
+
+import (
+ "encoding/json"
+ "net/http"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/errutil"
+)
+
+type MsgRes struct {
+ Code int
+ Msg string
+}
+
+var (
+ Err400 = MsgRes{Code: http.StatusBadRequest, Msg: "Bad Request"}
+ Err401 = MsgRes{Code: http.StatusUnauthorized, Msg: "Unauthorized"}
+ Err404 = MsgRes{Code: http.StatusNotFound, Msg: "Not Found"}
+ Err412 = MsgRes{Code: http.StatusPreconditionFailed, Msg: "Precondition Failed"}
+ Err429 = MsgRes{Code: http.StatusTooManyRequests, Msg: "Too Many Requests"}
+ Err500 = MsgRes{Code: http.StatusInternalServerError, Msg: "Internal Server Error"}
+ Err503 = MsgRes{Code: http.StatusServiceUnavailable, Msg: "Service Unavailable"}
+ Err504 = MsgRes{Code: http.StatusGatewayTimeout, Msg: "Gateway Timeout"}
+ Ok200 = MsgRes{Code: http.StatusOK, Msg: "OK"}
+)
+
+type HttpUtil interface {
+ GetCookie(cookies []*http.Cookie, key string) string
+ SetCookie(res http.ResponseWriter, key string, val string)
+ Fill(msg interface{}, res http.ResponseWriter) int
+}
+
+type QHttpUtil struct {
+ CookieDomain string
+ CookieHttpOnly bool
+ CookieMaxAge int
+ CookiePath string
+ CookieSecure bool
+ Err errutil.ErrUtil
+}
+
+func (q *QHttpUtil) GetCookie(cookies []*http.Cookie, key string) string {
+ for _, cookie := range cookies {
+ if cookie.Name == key {
+ return cookie.Value
+ }
+ }
+ return ""
+}
+
+func (q *QHttpUtil) SetCookie(res http.ResponseWriter, key string, val string) {
+ cookie := http.Cookie{
+ Name: key,
+ Value: val,
+ Domain: q.CookieDomain,
+ Expires: time.Now().Add(time.Duration(q.CookieMaxAge) * time.Second),
+ HttpOnly: q.CookieHttpOnly,
+ MaxAge: q.CookieMaxAge,
+ Secure: q.CookieSecure,
+ Path: q.CookiePath,
+ }
+
+ res.Header().Set("Set-Cookie", cookie.String())
+}
+
+func (q *QHttpUtil) Fill(msg interface{}, res http.ResponseWriter) int {
+ if msg == nil {
+ return 0
+ }
+
+ msgBytes, marsErr := json.Marshal(msg)
+ if q.Err.IsErr(marsErr) {
+ return 0
+ }
+
+ wrote, writeErr := res.Write(msgBytes)
+ if q.Err.IsErr(writeErr) {
+ return 0
+ }
+ return wrote
+}
diff --git a/server/libs/httpworker/worker.go b/server/libs/httpworker/worker.go
new file mode 100644
index 0000000..06aec0f
--- /dev/null
+++ b/server/libs/httpworker/worker.go
@@ -0,0 +1,130 @@
+package httpworker
+
+import (
+ "errors"
+ "net/http"
+ "runtime/debug"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/logutil"
+)
+
+var (
+ ErrWorkerNotFound = errors.New("worker not found")
+ ErrTimeout = errors.New("timeout")
+)
+
+type DoFunc func(http.ResponseWriter, *http.Request)
+
+type Task struct {
+ Ack chan error
+ Do DoFunc
+ Res http.ResponseWriter
+ Req *http.Request
+}
+
+type Workers interface {
+ Put(*Task) bool
+ IsInTime(ack chan error, msec time.Duration) error
+}
+
+type WorkerPool struct {
+ queue chan *Task
+ size int
+ workers []*Worker
+ log logutil.LogUtil // TODO: should not pass log here
+}
+
+func NewWorkerPool(poolSize int, queueSize int, log logutil.LogUtil) Workers {
+ queue := make(chan *Task, queueSize)
+ workers := make([]*Worker, 0, poolSize)
+
+ for i := 0; i < poolSize; i++ {
+ worker := &Worker{
+ Id: uint64(i),
+ queue: queue,
+ log: log,
+ }
+
+ go worker.Start()
+ workers = append(workers, worker)
+ }
+
+ return &WorkerPool{
+ queue: queue,
+ size: poolSize,
+ workers: workers,
+ log: log,
+ }
+}
+
+func (pool *WorkerPool) Put(task *Task) bool {
+ if len(pool.queue) >= pool.size {
+ return false
+ }
+
+ pool.queue <- task
+ return true
+}
+
+func (pool *WorkerPool) IsInTime(ack chan error, msec time.Duration) error {
+ start := time.Now().UnixNano()
+ timeout := make(chan error)
+
+ go func() {
+ time.Sleep(msec)
+ timeout <- ErrTimeout
+ }()
+
+ select {
+ case err := <-ack:
+ if err == nil {
+ pool.log.Printf(
+ "finish cost: %d usec",
+ (time.Now().UnixNano()-start)/1000,
+ )
+ } else {
+ pool.log.Printf(
+ "finish with error cost: %d usec",
+ (time.Now().UnixNano()-start)/1000,
+ )
+ }
+ return err
+ case errTimeout := <-timeout:
+ pool.log.Printf("timeout cost: %d usec", (time.Now().UnixNano()-start)/1000)
+ return errTimeout
+ }
+}
+
+type Worker struct {
+ Id uint64
+ queue chan *Task
+ log logutil.LogUtil
+}
+
+func (worker *Worker) RecoverPanic() {
+ if r := recover(); r != nil {
+ worker.log.Printf("Recovered:%v stack: %v", r, debug.Stack())
+ // restart worker and IsInTime will return timeout error for last task
+ worker.Start()
+ }
+}
+
+func (worker *Worker) Start() {
+ defer worker.RecoverPanic()
+
+ for {
+ task := <-worker.queue
+ if task.Do != nil {
+ task.Do(task.Res, task.Req)
+ task.Ack <- nil
+ } else {
+ task.Ack <- ErrWorkerNotFound
+ }
+ }
+}
+
+// ServiceFunc lets you return struct directly
+type ServiceFunc func(http.ResponseWriter, *http.Request) interface{}
diff --git a/server/libs/limiter/limiter.go b/server/libs/limiter/limiter.go
new file mode 100644
index 0000000..307037d
--- /dev/null
+++ b/server/libs/limiter/limiter.go
@@ -0,0 +1,5 @@
+package limiter
+
+type Limiter interface {
+ Access(string, int16) bool
+}
diff --git a/server/libs/limiter/rate_limiter.go b/server/libs/limiter/rate_limiter.go
new file mode 100644
index 0000000..7bccf1a
--- /dev/null
+++ b/server/libs/limiter/rate_limiter.go
@@ -0,0 +1,220 @@
+package limiter
+
+import (
+ "sync"
+ "time"
+)
+
+func now() int32 {
+ return int32(time.Now().Unix())
+}
+
+func afterCyc(cyc int32) int32 {
+ return int32(time.Now().Unix()) + cyc
+}
+
+func afterTtl(ttl int32) int32 {
+ return int32(time.Now().Unix()) + ttl
+}
+
+type Bucket struct {
+ Refresh int32
+ Tokens int16
+}
+
+func NewBucket(cyc int32, cap int16) *Bucket {
+ return &Bucket{
+ Refresh: afterCyc(cyc),
+ Tokens: cap,
+ }
+}
+
+type Item struct {
+ Expired int32
+ Buckets map[int16]*Bucket
+}
+
+func NewItem(ttl int32) *Item {
+ return &Item{
+ Expired: afterTtl(ttl),
+ Buckets: make(map[int16]*Bucket),
+ }
+}
+
+type RateLimiter struct {
+ items map[string]*Item
+ bucketCap int16
+ customCaps map[int16]int16
+ cap int64
+ cyc int32 // how much time, item autoclean will be executed, bucket will be refreshed
+ ttl int32 // how much time, item will be expired(but not cleaned)
+ mux sync.RWMutex
+ snapshot map[string]map[int16]*Bucket
+}
+
+func NewRateLimiter(cap int64, ttl int32, cyc int32, bucketCap int16, customCaps map[int16]int16) Limiter {
+ if cap < 1 || ttl < 1 || cyc < 1 || bucketCap < 1 {
+ panic("cap | bucketCap | ttl | cycle cant be less than 1")
+ }
+
+ limiter := &RateLimiter{
+ items: make(map[string]*Item, cap),
+ bucketCap: bucketCap,
+ customCaps: customCaps,
+ cap: cap,
+ ttl: ttl,
+ cyc: cyc,
+ }
+
+ go limiter.autoClean()
+
+ return limiter
+}
+
+func (limiter *RateLimiter) getBucketCap(opId int16) int16 {
+ bucketCap, existed := limiter.customCaps[opId]
+ if !existed {
+ return limiter.bucketCap
+ }
+ return bucketCap
+}
+
+func (limiter *RateLimiter) Access(itemId string, opId int16) bool {
+ limiter.mux.Lock()
+ defer limiter.mux.Unlock()
+
+ item, itemExisted := limiter.items[itemId]
+ if !itemExisted {
+ if int64(len(limiter.items)) >= limiter.cap {
+ return false
+ }
+
+ limiter.items[itemId] = NewItem(limiter.ttl)
+ limiter.items[itemId].Buckets[opId] = NewBucket(limiter.cyc, limiter.getBucketCap(opId)-1)
+ return true
+ }
+
+ bucket, bucketExisted := item.Buckets[opId]
+ if !bucketExisted {
+ item.Buckets[opId] = NewBucket(limiter.cyc, limiter.getBucketCap(opId)-1)
+ return true
+ }
+
+ if bucket.Refresh > now() {
+ if bucket.Tokens > 0 {
+ bucket.Tokens--
+ return true
+ }
+ return false
+ }
+
+ bucket.Refresh = afterCyc(limiter.cyc)
+ bucket.Tokens = limiter.getBucketCap(opId) - 1
+ return true
+}
+
+func (limiter *RateLimiter) GetCap() int64 {
+ return limiter.cap
+}
+
+func (limiter *RateLimiter) GetSize() int64 {
+ limiter.mux.RLock()
+ defer limiter.mux.RUnlock()
+ return int64(len(limiter.items))
+}
+
+func (limiter *RateLimiter) ExpandCap(cap int64) bool {
+ limiter.mux.RLock()
+ defer limiter.mux.RUnlock()
+
+ if cap <= int64(len(limiter.items)) {
+ return false
+ }
+
+ limiter.cap = cap
+ return true
+}
+
+func (limiter *RateLimiter) GetTTL() int32 {
+ return limiter.ttl
+}
+
+func (limiter *RateLimiter) UpdateTTL(ttl int32) bool {
+ if ttl < 1 {
+ return false
+ }
+
+ limiter.ttl = ttl
+ return true
+}
+
+func (limiter *RateLimiter) GetCyc() int32 {
+ return limiter.cyc
+}
+
+func (limiter *RateLimiter) UpdateCyc(cyc int32) bool {
+ if limiter.cyc < 1 {
+ return false
+ }
+
+ limiter.cyc = cyc
+ return true
+}
+
+func (limiter *RateLimiter) Snapshot() map[string]map[int16]*Bucket {
+ return limiter.snapshot
+}
+
+func (limiter *RateLimiter) autoClean() {
+ for {
+ if limiter.cyc == 0 {
+ break
+ }
+ time.Sleep(time.Duration(int64(limiter.cyc) * 1000000000))
+ limiter.clean()
+ }
+}
+
+// clean may add affect other operations, do frequently?
+func (limiter *RateLimiter) clean() {
+ limiter.snapshot = make(map[string]map[int16]*Bucket)
+ now := now()
+
+ limiter.mux.RLock()
+ defer limiter.mux.RUnlock()
+ for key, item := range limiter.items {
+ if item.Expired <= now {
+ delete(limiter.items, key)
+ } else {
+ limiter.snapshot[key] = item.Buckets
+ }
+ }
+}
+
+// Only for test
+func (limiter *RateLimiter) exist(id string) bool {
+ limiter.mux.RLock()
+ defer limiter.mux.RUnlock()
+
+ _, existed := limiter.items[id]
+ return existed
+}
+
+// Only for test
+func (limiter *RateLimiter) truncate() {
+ limiter.mux.RLock()
+ defer limiter.mux.RUnlock()
+
+ for key, _ := range limiter.items {
+ delete(limiter.items, key)
+ }
+}
+
+// Only for test
+func (limiter *RateLimiter) get(id string) (*Item, bool) {
+ limiter.mux.RLock()
+ defer limiter.mux.RUnlock()
+
+ item, existed := limiter.items[id]
+ return item, existed
+}
diff --git a/server/libs/limiter/rate_limiter_test.go b/server/libs/limiter/rate_limiter_test.go
new file mode 100644
index 0000000..3f90dcf
--- /dev/null
+++ b/server/libs/limiter/rate_limiter_test.go
@@ -0,0 +1,161 @@
+package limiter
+
+import (
+ "fmt"
+ "math/rand"
+ "testing"
+ "time"
+)
+
+var rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
+
+const rndCap = 10000
+const addCap = 1
+
+// how to set time
+// extend: wait can be greater than ttl/2
+// cyc is smaller than ttl and wait, then it can be clean in time
+const cap = 40
+const ttl = 3
+const cyc = 1
+const bucketCap = 2
+const id1 = "id1"
+const id2 = "id2"
+const op1 int16 = 0
+const op2 int16 = 1
+
+var customCaps = map[int16]int16{
+ op2: 1000,
+}
+
+const wait = 1
+
+var limiter = NewRateLimiter(cap, ttl, cyc, bucketCap, customCaps).(*RateLimiter)
+
+func printItem(id string) {
+ item, existed := limiter.get(id1)
+ if existed {
+ fmt.Println("expired, now, existed", item.Expired, now(), existed)
+ for id, bucket := range item.Buckets {
+ fmt.Println("\tid, bucket", id, bucket)
+ }
+ } else {
+ fmt.Println("not existed")
+ }
+}
+
+var idSeed = 0
+
+func randId() string {
+ idSeed++
+ return fmt.Sprintf("%d", idSeed)
+}
+
+func TestAccess(t *testing.T) {
+ func(t *testing.T) {
+ canAccess := limiter.Access(id1, op1)
+ if !canAccess {
+ t.Fatal("access: fail")
+ }
+
+ for i := 0; i < bucketCap; i++ {
+ canAccess = limiter.Access(id1, op1)
+ }
+
+ if canAccess {
+ t.Fatal("access: fail to deny access")
+ }
+
+ time.Sleep(time.Duration(limiter.GetCyc()) * time.Second)
+
+ canAccess = limiter.Access(id1, op1)
+ if !canAccess {
+ t.Fatal("access: fail to refresh tokens")
+ }
+ }(t)
+}
+
+func TestCap(t *testing.T) {
+ originalCap := limiter.GetCap()
+ fmt.Printf("cap:info: %d\n", originalCap)
+
+ ok := limiter.ExpandCap(originalCap + addCap)
+
+ if !ok || limiter.GetCap() != originalCap+addCap {
+ t.Fatal("cap: fail to expand")
+ }
+
+ ok = limiter.ExpandCap(limiter.GetSize() - addCap)
+ if ok {
+ t.Fatal("cap: shrink cap")
+ }
+
+ ids := []string{}
+ for limiter.GetSize() < limiter.GetCap() {
+ id := randId()
+ ids = append(ids, id)
+
+ ok := limiter.Access(id, 0)
+ if !ok {
+ t.Fatal("cap: not full")
+ }
+ }
+
+ if limiter.GetSize() != limiter.GetCap() {
+ t.Fatal("cap: incorrect size")
+ }
+
+ if limiter.Access(randId(), 0) {
+ t.Fatal("cap: more than cap")
+ }
+
+ limiter.truncate()
+}
+
+func TestTtl(t *testing.T) {
+ var addTtl int32 = 1
+ originalTTL := limiter.GetTTL()
+ fmt.Printf("ttl:info: %d\n", originalTTL)
+
+ limiter.UpdateTTL(originalTTL + addTtl)
+ if limiter.GetTTL() != originalTTL+addTtl {
+ t.Fatal("ttl: update fail")
+ }
+}
+
+func cycTest(t *testing.T) {
+ var addCyc int32 = 1
+ originalCyc := limiter.GetCyc()
+ fmt.Printf("cyc:info: %d\n", originalCyc)
+
+ limiter.UpdateCyc(originalCyc + addCyc)
+ if limiter.GetCyc() != originalCyc+addCyc {
+ t.Fatal("cyc: update fail")
+ }
+}
+
+func autoCleanTest(t *testing.T) {
+ ids := []string{
+ randId(),
+ randId(),
+ }
+
+ for _, id := range ids {
+ ok := limiter.Access(id, 0)
+ if ok {
+ t.Fatal("autoClean: warning: add fail")
+ }
+ }
+
+ time.Sleep(time.Duration(limiter.GetTTL()+wait) * time.Second)
+
+ for _, id := range ids {
+ _, exist := limiter.get(id)
+ if exist {
+ t.Fatal("autoClean: item still exist")
+ }
+ }
+}
+
+// func snapshotTest(t *testing.T) {
+// }
diff --git a/server/libs/logutil/logutil.go b/server/libs/logutil/logutil.go
new file mode 100644
index 0000000..15b2f6a
--- /dev/null
+++ b/server/libs/logutil/logutil.go
@@ -0,0 +1,7 @@
+package logutil
+
+type LogUtil interface {
+ Print(v ...interface{})
+ Printf(format string, v ...interface{})
+ Println(v ...interface{})
+}
diff --git a/server/libs/logutil/slogger.go b/server/libs/logutil/slogger.go
new file mode 100644
index 0000000..f0cd3f6
--- /dev/null
+++ b/server/libs/logutil/slogger.go
@@ -0,0 +1,12 @@
+package logutil
+
+import (
+ "io"
+ "log"
+)
+
+func NewSlog(out io.Writer, prefix string) LogUtil {
+ return log.New(out, prefix, log.Ldate|log.Ltime|log.Lshortfile)
+}
+
+type Slog *log.Logger
diff --git a/server/libs/qtube/downloader.go b/server/libs/qtube/downloader.go
new file mode 100644
index 0000000..92a425d
--- /dev/null
+++ b/server/libs/qtube/downloader.go
@@ -0,0 +1,13 @@
+package qtube
+
+import (
+ "net/http"
+)
+
+import (
+ "quickshare/server/libs/fileidx"
+)
+
+type Downloader interface {
+ ServeFile(res http.ResponseWriter, req *http.Request, fileInfo *fileidx.FileInfo) error
+}
diff --git a/server/libs/qtube/qtube.go b/server/libs/qtube/qtube.go
new file mode 100644
index 0000000..bd43825
--- /dev/null
+++ b/server/libs/qtube/qtube.go
@@ -0,0 +1,280 @@
+package qtube
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/fileidx"
+)
+
+var (
+ ErrCopy = errors.New("ServeFile: copy error")
+ ErrUnknown = errors.New("ServeFile: unknown error")
+)
+
+type httpRange struct {
+ start, length int64
+}
+
+func (ra *httpRange) GetStart() int64 {
+ return ra.start
+}
+func (ra *httpRange) GetLength() int64 {
+ return ra.length
+}
+func (ra *httpRange) SetStart(start int64) {
+ ra.start = start
+}
+func (ra *httpRange) SetLength(length int64) {
+ ra.length = length
+}
+
+func NewQTube(root string, copySpeed, maxRangeLen int64, filer FileReadSeekCloser) Downloader {
+ return &QTube{
+ Root: root,
+ BytesPerSec: copySpeed,
+ MaxRangeLen: maxRangeLen,
+ Filer: filer,
+ }
+}
+
+type QTube struct {
+ Root string
+ BytesPerSec int64
+ MaxRangeLen int64
+ Filer FileReadSeekCloser
+}
+
+type FileReadSeekCloser interface {
+ Open(filePath string) (ReadSeekCloser, error)
+}
+
+type ReadSeekCloser interface {
+ io.Reader
+ io.Seeker
+ io.Closer
+}
+
+const (
+ ErrorInvalidRange = "ServeFile: invalid Range"
+ ErrorInvalidSize = "ServeFile: invalid Range total size"
+)
+
+func (tb *QTube) ServeFile(res http.ResponseWriter, req *http.Request, fileInfo *fileidx.FileInfo) error {
+ headerRange := req.Header.Get("Range")
+
+ switch {
+ case req.Method == http.MethodHead:
+ res.Header().Set("Accept-Ranges", "bytes")
+ res.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Uploaded))
+ res.Header().Set("Content-Type", "application/octet-stream")
+ res.WriteHeader(http.StatusOK)
+
+ return nil
+ case headerRange == "":
+ return tb.serveAll(res, fileInfo)
+ default:
+ return tb.serveRanges(res, headerRange, fileInfo)
+ }
+}
+
+func (tb *QTube) serveAll(res http.ResponseWriter, fileInfo *fileidx.FileInfo) error {
+ res.Header().Set("Accept-Ranges", "bytes")
+ res.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(fileInfo.PathLocal)))
+ res.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Uploaded))
+ res.Header().Set("Content-Type", "application/octet-stream")
+ res.Header().Set("Last-Modified", time.Unix(fileInfo.ModTime, 0).UTC().Format(http.TimeFormat))
+ res.WriteHeader(http.StatusOK)
+
+ // TODO: need verify path
+ file, openErr := tb.Filer.Open(filepath.Join(tb.Root, fileInfo.PathLocal))
+ defer file.Close()
+ if openErr != nil {
+ return openErr
+ }
+
+ copyErr := tb.throttledCopyN(res, file, fileInfo.Uploaded)
+ if copyErr != nil && copyErr != io.EOF {
+ return copyErr
+ }
+
+ return nil
+}
+
+func (tb *QTube) serveRanges(res http.ResponseWriter, headerRange string, fileInfo *fileidx.FileInfo) error {
+ ranges, rangeErr := getRanges(headerRange, fileInfo.Uploaded)
+ if rangeErr != nil {
+ http.Error(res, rangeErr.Error(), http.StatusRequestedRangeNotSatisfiable)
+ return errors.New(rangeErr.Error())
+ }
+
+ switch {
+ case len(ranges) == 1 || len(ranges) > 1:
+ if tb.copyRange(res, ranges[0], fileInfo) != nil {
+ return ErrCopy
+ }
+ default:
+ // TODO: add support for multiple ranges
+ return ErrUnknown
+ }
+
+ return nil
+}
+
+func getRanges(headerRange string, size int64) ([]httpRange, error) {
+ ranges, raParseErr := parseRange(headerRange, size)
+ // TODO: check max number of ranges, range start end
+ if len(ranges) <= 0 || raParseErr != nil {
+ return nil, errors.New(ErrorInvalidRange)
+ }
+ if sumRangesSize(ranges) > size {
+ return nil, errors.New(ErrorInvalidSize)
+ }
+
+ return ranges, nil
+}
+
+func (tb *QTube) copyRange(res http.ResponseWriter, ra httpRange, fileInfo *fileidx.FileInfo) error {
+ // TODO: comfirm this wont cause problem
+ if ra.GetLength() > tb.MaxRangeLen {
+ ra.SetLength(tb.MaxRangeLen)
+ }
+
+ // TODO: add headers(ETag): https://tools.ietf.org/html/rfc7233#section-4.1 p11 2nd paragraph
+ res.Header().Set("Accept-Ranges", "bytes")
+ res.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(fileInfo.PathLocal)))
+ res.Header().Set("Content-Type", "application/octet-stream")
+ res.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, fileInfo.Uploaded))
+ res.Header().Set("Content-Length", strconv.FormatInt(ra.GetLength(), 10))
+ res.Header().Set("Last-Modified", time.Unix(fileInfo.ModTime, 0).UTC().Format(http.TimeFormat))
+ res.WriteHeader(http.StatusPartialContent)
+
+ // TODO: need verify path
+ file, openErr := tb.Filer.Open(filepath.Join(tb.Root, fileInfo.PathLocal))
+ defer file.Close()
+ if openErr != nil {
+ return openErr
+ }
+
+ if _, seekErr := file.Seek(ra.start, io.SeekStart); seekErr != nil {
+ return seekErr
+ }
+
+ copyErr := tb.throttledCopyN(res, file, ra.length)
+ if copyErr != nil && copyErr != io.EOF {
+ return copyErr
+ }
+
+ return nil
+}
+
+func (tb *QTube) throttledCopyN(dst io.Writer, src io.Reader, length int64) error {
+ sum := int64(0)
+ timeSlot := time.Duration(1 * time.Second)
+
+ for sum < length {
+ start := time.Now()
+ chunkSize := length - sum
+ if length-sum > tb.BytesPerSec {
+ chunkSize = tb.BytesPerSec
+ }
+
+ copied, err := io.CopyN(dst, src, chunkSize)
+ if err != nil {
+ return err
+ }
+
+ sum += copied
+ end := time.Now()
+ if end.Before(start.Add(timeSlot)) {
+ time.Sleep(start.Add(timeSlot).Sub(end))
+ }
+ }
+
+ return nil
+}
+
+func parseRange(headerRange string, size int64) ([]httpRange, error) {
+ if headerRange == "" {
+ return nil, nil // header not present
+ }
+
+ const keyByte = "bytes="
+ if !strings.HasPrefix(headerRange, keyByte) {
+ return nil, errors.New("byte= not found")
+ }
+
+ var ranges []httpRange
+ noOverlap := false
+ for _, ra := range strings.Split(headerRange[len(keyByte):], ",") {
+ ra = strings.TrimSpace(ra)
+ if ra == "" {
+ continue
+ }
+
+ i := strings.Index(ra, "-")
+ if i < 0 {
+ return nil, errors.New("- not found")
+ }
+
+ start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:])
+ var r httpRange
+ if start == "" {
+ i, err := strconv.ParseInt(end, 10, 64)
+ if err != nil {
+ return nil, errors.New("invalid range")
+ }
+ if i > size {
+ i = size
+ }
+ r.start = size - i
+ r.length = size - r.start
+ } else {
+ i, err := strconv.ParseInt(start, 10, 64)
+ if err != nil || i < 0 {
+ return nil, errors.New("invalid range")
+ }
+ if i >= size {
+ // If the range begins after the size of the content,
+ // then it does not overlap.
+ noOverlap = true
+ continue
+ }
+ r.start = i
+ if end == "" {
+ // If no end is specified, range extends to end of the file.
+ r.length = size - r.start
+ } else {
+ i, err := strconv.ParseInt(end, 10, 64)
+ if err != nil || r.start > i {
+ return nil, errors.New("invalid range")
+ }
+ if i >= size {
+ i = size - 1
+ }
+ r.length = i - r.start + 1
+ }
+ }
+ ranges = append(ranges, r)
+ }
+ if noOverlap && len(ranges) == 0 {
+ // The specified ranges did not overlap with the content.
+ return nil, errors.New("parseRanges: no overlap")
+ }
+ return ranges, nil
+}
+
+func sumRangesSize(ranges []httpRange) (size int64) {
+ for _, ra := range ranges {
+ size += ra.length
+ }
+ return
+}
diff --git a/server/libs/qtube/qtube_test.go b/server/libs/qtube/qtube_test.go
new file mode 100644
index 0000000..00f95f3
--- /dev/null
+++ b/server/libs/qtube/qtube_test.go
@@ -0,0 +1,354 @@
+package qtube
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/fileidx"
+)
+
+// Range format examples:
+// Range: =-
+// Range: =-
+// Range: =-, -
+// Range: =-, -, -
+func TestGetRanges(t *testing.T) {
+ type Input struct {
+ HeaderRange string
+ Size int64
+ }
+ type Output struct {
+ Ranges []httpRange
+ ErrorMsg string
+ }
+ type testCase struct {
+ Desc string
+ Input
+ Output
+ }
+
+ testCases := []testCase{
+ testCase{
+ Desc: "invalid range",
+ Input: Input{
+ HeaderRange: "bytes=start-invalid end",
+ Size: 0,
+ },
+ Output: Output{
+ ErrorMsg: ErrorInvalidRange,
+ },
+ },
+ testCase{
+ Desc: "invalid range total size",
+ Input: Input{
+ HeaderRange: "bytes=0-1, 2-3, 0-1, 0-2",
+ Size: 3,
+ },
+ Output: Output{
+ ErrorMsg: ErrorInvalidSize,
+ },
+ },
+ testCase{
+ Desc: "range ok",
+ Input: Input{
+ HeaderRange: "bytes=0-1, 2-3",
+ Size: 4,
+ },
+ Output: Output{
+ Ranges: []httpRange{
+ httpRange{start: 0, length: 2},
+ httpRange{start: 2, length: 2},
+ },
+ ErrorMsg: "",
+ },
+ },
+ }
+
+ for _, tCase := range testCases {
+ ranges, err := getRanges(tCase.HeaderRange, tCase.Size)
+ if err != nil {
+ if err.Error() != tCase.ErrorMsg || len(tCase.Ranges) != 0 {
+ t.Fatalf("getRanges: incorrect errorMsg want: %v got: %v", tCase.ErrorMsg, err.Error())
+ } else {
+ continue
+ }
+ } else {
+ for id, ra := range ranges {
+ if ra.GetStart() != tCase.Ranges[id].GetStart() {
+ t.Fatalf("getRanges: incorrect range start, got: %v want: %v", ra.GetStart(), tCase.Ranges[id])
+ }
+ if ra.GetLength() != tCase.Ranges[id].GetLength() {
+ t.Fatalf("getRanges: incorrect range length, got: %v want: %v", ra.GetLength(), tCase.Ranges[id])
+ }
+ }
+ }
+ }
+}
+
+func TestThrottledCopyN(t *testing.T) {
+ type Init struct {
+ BytesPerSec int64
+ MaxRangeLen int64
+ }
+ type Input struct {
+ Src string
+ Length int64
+ }
+ // after starting throttledCopyN by DstAtTime.AtMs millisecond,
+ // copied valueshould equal to DstAtTime.Dst.
+ type DstAtTime struct {
+ AtMS int
+ Dst string
+ }
+ type Output struct {
+ ExpectDsts []DstAtTime
+ }
+ type testCase struct {
+ Desc string
+ Init
+ Input
+ Output
+ }
+
+ verifyDsts := func(dst *bytes.Buffer, expectDsts []DstAtTime) {
+ for _, expectDst := range expectDsts {
+ // fmt.Printf("sleep: %d\n", time.Now().UnixNano())
+ time.Sleep(time.Duration(expectDst.AtMS) * time.Millisecond)
+ dstStr := string(dst.Bytes())
+ // fmt.Printf("check: %d\n", time.Now().UnixNano())
+ if dstStr != expectDst.Dst {
+ panic(
+ fmt.Sprintf(
+ "throttledCopyN want: <%s> | got: <%s> | at: %d",
+ expectDst.Dst,
+ dstStr,
+ expectDst.AtMS,
+ ),
+ )
+ }
+ }
+ }
+
+ testCases := []testCase{
+ testCase{
+ Desc: "4 byte per sec",
+ Init: Init{
+ BytesPerSec: 5,
+ MaxRangeLen: 10,
+ },
+ Input: Input{
+ Src: "aaaa_aaaa_",
+ Length: 10,
+ },
+ Output: Output{
+ ExpectDsts: []DstAtTime{
+ DstAtTime{AtMS: 200, Dst: "aaaa_"},
+ DstAtTime{AtMS: 200, Dst: "aaaa_"},
+ DstAtTime{AtMS: 200, Dst: "aaaa_"},
+ DstAtTime{AtMS: 600, Dst: "aaaa_aaaa_"},
+ DstAtTime{AtMS: 200, Dst: "aaaa_aaaa_"},
+ DstAtTime{AtMS: 200, Dst: "aaaa_aaaa_"},
+ },
+ },
+ },
+ }
+
+ for _, tCase := range testCases {
+ tb := NewQTube("", tCase.BytesPerSec, tCase.MaxRangeLen, &stubFiler{}).(*QTube)
+ dst := bytes.NewBuffer(make([]byte, len(tCase.Src)))
+ dst.Reset()
+
+ go verifyDsts(dst, tCase.ExpectDsts)
+ tb.throttledCopyN(dst, strings.NewReader(tCase.Src), tCase.Length)
+ }
+}
+
+// TODO: using same stub with testhelper
+type stubWriter struct {
+ Headers http.Header
+ Response []byte
+ StatusCode int
+}
+
+func (w *stubWriter) Header() http.Header {
+ return w.Headers
+}
+
+func (w *stubWriter) Write(body []byte) (int, error) {
+ w.Response = append(w.Response, body...)
+ return len(body), nil
+}
+
+func (w *stubWriter) WriteHeader(statusCode int) {
+ w.StatusCode = statusCode
+}
+
+func TestCopyRange(t *testing.T) {
+ type Init struct {
+ Content string
+ }
+ type Input struct {
+ Range httpRange
+ Info fileidx.FileInfo
+ }
+ type Output struct {
+ StatusCode int
+ Headers map[string][]string
+ Body string
+ }
+ type testCase struct {
+ Desc string
+ Init
+ Input
+ Output
+ }
+
+ testCases := []testCase{
+ testCase{
+ Desc: "copy ok",
+ Init: Init{
+ Content: "abcd_abcd_",
+ },
+ Input: Input{
+ Range: httpRange{
+ start: 6,
+ length: 3,
+ },
+ Info: fileidx.FileInfo{
+ ModTime: 0,
+ Uploaded: 10,
+ PathLocal: "filename.jpg",
+ },
+ },
+ Output: Output{
+ StatusCode: 206,
+ Headers: map[string][]string{
+ "Accept-Ranges": []string{"bytes"},
+ "Content-Disposition": []string{`attachment; filename="filename.jpg"`},
+ "Content-Type": []string{"application/octet-stream"},
+ "Content-Range": []string{"bytes 6-8/10"},
+ "Content-Length": []string{"3"},
+ "Last-Modified": []string{time.Unix(0, 0).UTC().Format(http.TimeFormat)},
+ },
+ Body: "abc",
+ },
+ },
+ }
+
+ for _, tCase := range testCases {
+ filer := &stubFiler{
+ &StubFile{
+ Content: tCase.Content,
+ Offset: 0,
+ },
+ }
+ tb := NewQTube("", 100, 100, filer).(*QTube)
+ res := &stubWriter{
+ Headers: make(map[string][]string),
+ Response: make([]byte, 0),
+ }
+ err := tb.copyRange(res, tCase.Range, &tCase.Info)
+ if err != nil {
+ t.Fatalf("copyRange: %v", err)
+ }
+ if res.StatusCode != tCase.Output.StatusCode {
+ t.Fatalf("copyRange: statusCode not match got: %v want: %v", res.StatusCode, tCase.Output.StatusCode)
+ }
+ if string(res.Response) != tCase.Output.Body {
+ t.Fatalf("copyRange: body not match \ngot: %v \nwant: %v", string(res.Response), tCase.Output.Body)
+ }
+ for key, vals := range tCase.Output.Headers {
+ if res.Header().Get(key) != vals[0] {
+ t.Fatalf("copyRange: header not match %v got: %v want: %v", key, res.Header().Get(key), vals[0])
+ }
+ }
+ if res.StatusCode != tCase.Output.StatusCode {
+ t.Fatalf("copyRange: statusCodes are not match %v", res.StatusCode, tCase.Output.StatusCode)
+ }
+ }
+}
+
+func TestServeAll(t *testing.T) {
+ type Init struct {
+ Content string
+ }
+ type Input struct {
+ Info fileidx.FileInfo
+ }
+ type Output struct {
+ StatusCode int
+ Headers map[string][]string
+ Body string
+ }
+ type testCase struct {
+ Desc string
+ Init
+ Input
+ Output
+ }
+
+ testCases := []testCase{
+ testCase{
+ Desc: "copy ok",
+ Init: Init{
+ Content: "abcd_abcd_",
+ },
+ Input: Input{
+ Info: fileidx.FileInfo{
+ ModTime: 0,
+ Uploaded: 10,
+ PathLocal: "filename.jpg",
+ },
+ },
+ Output: Output{
+ StatusCode: 200,
+ Headers: map[string][]string{
+ "Accept-Ranges": []string{"bytes"},
+ "Content-Disposition": []string{`attachment; filename="filename.jpg"`},
+ "Content-Type": []string{"application/octet-stream"},
+ "Content-Length": []string{"10"},
+ "Last-Modified": []string{time.Unix(0, 0).UTC().Format(http.TimeFormat)},
+ },
+ Body: "abcd_abcd_",
+ },
+ },
+ }
+
+ for _, tCase := range testCases {
+ filer := &stubFiler{
+ &StubFile{
+ Content: tCase.Content,
+ Offset: 0,
+ },
+ }
+ tb := NewQTube("", 100, 100, filer).(*QTube)
+ res := &stubWriter{
+ Headers: make(map[string][]string),
+ Response: make([]byte, 0),
+ }
+ err := tb.serveAll(res, &tCase.Info)
+ if err != nil {
+ t.Fatalf("serveAll: %v", err)
+ }
+ if res.StatusCode != tCase.Output.StatusCode {
+ t.Fatalf("serveAll: statusCode not match got: %v want: %v", res.StatusCode, tCase.Output.StatusCode)
+ }
+ if string(res.Response) != tCase.Output.Body {
+ t.Fatalf("serveAll: body not match \ngot: %v \nwant: %v", string(res.Response), tCase.Output.Body)
+ }
+ for key, vals := range tCase.Output.Headers {
+ if res.Header().Get(key) != vals[0] {
+ t.Fatalf("serveAll: header not match %v got: %v want: %v", key, res.Header().Get(key), vals[0])
+ }
+ }
+ if res.StatusCode != tCase.Output.StatusCode {
+ t.Fatalf("serveAll: statusCodes are not match %v", res.StatusCode, tCase.Output.StatusCode)
+ }
+ }
+}
diff --git a/server/libs/qtube/test_helper.go b/server/libs/qtube/test_helper.go
new file mode 100644
index 0000000..e0fa910
--- /dev/null
+++ b/server/libs/qtube/test_helper.go
@@ -0,0 +1,28 @@
+package qtube
+
+type StubFile struct {
+ Content string
+ Offset int64
+}
+
+func (file *StubFile) Read(p []byte) (int, error) {
+ copied := copy(p[:], []byte(file.Content)[:len(p)])
+ return copied, nil
+}
+
+func (file *StubFile) Seek(offset int64, whence int) (int64, error) {
+ file.Offset = offset
+ return offset, nil
+}
+
+func (file *StubFile) Close() error {
+ return nil
+}
+
+type stubFiler struct {
+ file *StubFile
+}
+
+func (filer *stubFiler) Open(filePath string) (ReadSeekCloser, error) {
+ return filer.file, nil
+}
diff --git a/server/libs/walls/access_walls.go b/server/libs/walls/access_walls.go
new file mode 100644
index 0000000..e1d3b65
--- /dev/null
+++ b/server/libs/walls/access_walls.go
@@ -0,0 +1,102 @@
+package walls
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/cfg"
+ "quickshare/server/libs/encrypt"
+ "quickshare/server/libs/limiter"
+)
+
+type AccessWalls struct {
+ cf *cfg.Config
+ IpLimiter limiter.Limiter
+ OpLimiter limiter.Limiter
+ EncrypterMaker encrypt.EncrypterMaker
+}
+
+func NewAccessWalls(
+ cf *cfg.Config,
+ ipLimiter limiter.Limiter,
+ opLimiter limiter.Limiter,
+ encrypterMaker encrypt.EncrypterMaker,
+) Walls {
+ return &AccessWalls{
+ cf: cf,
+ IpLimiter: ipLimiter,
+ OpLimiter: opLimiter,
+ EncrypterMaker: encrypterMaker,
+ }
+}
+
+func (walls *AccessWalls) PassIpLimit(remoteAddr string) bool {
+ if !walls.cf.Production {
+ return true
+ }
+ return walls.IpLimiter.Access(remoteAddr, walls.cf.OpIdIpVisit)
+
+}
+
+func (walls *AccessWalls) PassOpLimit(resourceId string, opId int16) bool {
+ if !walls.cf.Production {
+ return true
+ }
+ return walls.OpLimiter.Access(resourceId, opId)
+}
+
+func (walls *AccessWalls) PassLoginCheck(tokenStr string, req *http.Request) bool {
+ if !walls.cf.Production {
+ return true
+ }
+
+ return walls.passLoginCheck(tokenStr)
+}
+
+func (walls *AccessWalls) passLoginCheck(tokenStr string) bool {
+ token, getLoginTokenOk := walls.GetLoginToken(tokenStr)
+ return getLoginTokenOk && token.AdminId == walls.cf.AdminId
+}
+
+func (walls *AccessWalls) GetLoginToken(tokenStr string) (*LoginToken, bool) {
+ tokenMaker := walls.EncrypterMaker(string(walls.cf.SecretKeyByte))
+ if !tokenMaker.FromStr(tokenStr) {
+ return nil, false
+ }
+
+ adminIdFromToken, adminIdOk := tokenMaker.Get(walls.cf.KeyAdminId)
+ expiresStr, expiresStrOk := tokenMaker.Get(walls.cf.KeyExpires)
+ if !adminIdOk || !expiresStrOk {
+ return nil, false
+ }
+
+ expires, expiresParseErr := strconv.ParseInt(expiresStr, 10, 64)
+ if expiresParseErr != nil ||
+ adminIdFromToken != walls.cf.AdminId ||
+ expires <= time.Now().Unix() {
+ return nil, false
+ }
+
+ return &LoginToken{
+ AdminId: adminIdFromToken,
+ Expires: expires,
+ }, true
+}
+
+func (walls *AccessWalls) MakeLoginToken(userId string) string {
+ expires := time.Now().Add(time.Duration(walls.cf.CookieMaxAge) * time.Second).Unix()
+
+ tokenMaker := walls.EncrypterMaker(string(walls.cf.SecretKeyByte))
+ tokenMaker.Add(walls.cf.KeyAdminId, userId)
+ tokenMaker.Add(walls.cf.KeyExpires, fmt.Sprintf("%d", expires))
+
+ tokenStr, ok := tokenMaker.ToStr()
+ if !ok {
+ return ""
+ }
+ return tokenStr
+}
diff --git a/server/libs/walls/access_walls_test.go b/server/libs/walls/access_walls_test.go
new file mode 100644
index 0000000..5e3afad
--- /dev/null
+++ b/server/libs/walls/access_walls_test.go
@@ -0,0 +1,145 @@
+package walls
+
+import (
+ "fmt"
+ "testing"
+ "time"
+)
+
+import (
+ "quickshare/server/libs/cfg"
+ "quickshare/server/libs/encrypt"
+ "quickshare/server/libs/limiter"
+)
+
+func newAccessWalls(limiterCap int64, limiterTtl int32, limiterCyc int32, bucketCap int16) *AccessWalls {
+ config := cfg.NewConfig()
+ config.Production = true
+ config.LimiterCap = limiterCap
+ config.LimiterTtl = limiterTtl
+ config.LimiterCyc = limiterCyc
+ config.BucketCap = bucketCap
+ encrypterMaker := encrypt.JwtEncrypterMaker
+ ipLimiter := limiter.NewRateLimiter(config.LimiterCap, config.LimiterTtl, config.LimiterCyc, config.BucketCap, map[int16]int16{})
+ opLimiter := limiter.NewRateLimiter(config.LimiterCap, config.LimiterTtl, config.LimiterCyc, config.BucketCap, map[int16]int16{})
+
+ return NewAccessWalls(config, ipLimiter, opLimiter, encrypterMaker).(*AccessWalls)
+}
+func TestIpLimit(t *testing.T) {
+ ip := "0.0.0.0"
+ limit := int16(10)
+ ttl := int32(60)
+ cyc := int32(5)
+ walls := newAccessWalls(1000, ttl, cyc, limit)
+
+ testIpLimit(t, walls, ip, limit)
+ // wait for tokens are re-fullfilled
+ time.Sleep(time.Duration(cyc) * time.Second)
+ testIpLimit(t, walls, ip, limit)
+
+ fmt.Println("ip limit: passed")
+}
+
+func testIpLimit(t *testing.T, walls Walls, ip string, limit int16) {
+ for i := int16(0); i < limit; i++ {
+ if !walls.PassIpLimit(ip) {
+ t.Fatalf("ipLimiter: should be passed", time.Now().Unix())
+ }
+ }
+
+ if walls.PassIpLimit(ip) {
+ t.Fatalf("ipLimiter: should not be passed", time.Now().Unix())
+ }
+}
+
+func TestOpLimit(t *testing.T) {
+ resourceId := "id"
+ op1 := int16(1)
+ op2 := int16(2)
+ limit := int16(10)
+ ttl := int32(1)
+ walls := newAccessWalls(1000, 5, ttl, limit)
+
+ testOpLimit(t, walls, resourceId, op1, limit)
+ testOpLimit(t, walls, resourceId, op2, limit)
+ // wait for tokens are re-fullfilled
+ time.Sleep(time.Duration(ttl) * time.Second)
+ testOpLimit(t, walls, resourceId, op1, limit)
+ testOpLimit(t, walls, resourceId, op2, limit)
+
+ fmt.Println("op limit: passed")
+}
+
+func testOpLimit(t *testing.T, walls Walls, resourceId string, op int16, limit int16) {
+ for i := int16(0); i < limit; i++ {
+ if !walls.PassOpLimit(resourceId, op) {
+ t.Fatalf("opLimiter: should be passed")
+ }
+ }
+
+ if walls.PassOpLimit(resourceId, op) {
+ t.Fatalf("opLimiter: should not be passed")
+ }
+}
+
+func TestLoginCheck(t *testing.T) {
+ walls := newAccessWalls(1000, 5, 1, 10)
+
+ testValidToken(t, walls)
+ testInvalidAdminIdToken(t, walls)
+ testExpiredToken(t, walls)
+}
+
+func testValidToken(t *testing.T, walls *AccessWalls) {
+ config := cfg.NewConfig()
+
+ tokenMaker := encrypt.JwtEncrypterMaker(string(config.SecretKeyByte))
+ tokenMaker.Add(config.KeyAdminId, config.AdminId)
+ tokenMaker.Add(config.KeyExpires, fmt.Sprintf("%d", time.Now().Unix()+int64(10)))
+ tokenStr, getTokenOk := tokenMaker.ToStr()
+ if !getTokenOk {
+ t.Fatalf("passLoginCheck: fail to generate token")
+ }
+
+ if !walls.passLoginCheck(tokenStr) {
+ t.Fatalf("loginCheck: should be passed")
+ }
+
+ fmt.Println("loginCheck: valid token passed")
+}
+
+func testInvalidAdminIdToken(t *testing.T, walls *AccessWalls) {
+ config := cfg.NewConfig()
+
+ tokenMaker := encrypt.JwtEncrypterMaker(string(config.SecretKeyByte))
+ tokenMaker.Add(config.KeyAdminId, "invalid admin id")
+ tokenMaker.Add(config.KeyExpires, fmt.Sprintf("%d", time.Now().Unix()+int64(10)))
+ tokenStr, getTokenOk := tokenMaker.ToStr()
+ if !getTokenOk {
+ t.Fatalf("passLoginCheck: fail to generate token")
+ }
+
+ if walls.passLoginCheck(tokenStr) {
+ t.Fatalf("loginCheck: should not be passed")
+ }
+
+ fmt.Println("loginCheck: invalid admin id passed")
+}
+
+func testExpiredToken(t *testing.T, walls *AccessWalls) {
+ config := cfg.NewConfig()
+
+ tokenMaker := encrypt.JwtEncrypterMaker(string(config.SecretKeyByte))
+ tokenMaker.Add(config.KeyAdminId, config.AdminId)
+ tokenMaker.Add(config.KeyExpires, fmt.Sprintf("%d", time.Now().Unix()-int64(1)))
+ tokenStr, getTokenOk := tokenMaker.ToStr()
+ if !getTokenOk {
+ t.Fatalf("passLoginCheck: fail to generate token")
+ }
+
+ if walls.passLoginCheck(tokenStr) {
+ t.Fatalf("loginCheck: should not be passed")
+ }
+
+ fmt.Println("loginCheck: expired token passed")
+}
diff --git a/server/libs/walls/walls.go b/server/libs/walls/walls.go
new file mode 100644
index 0000000..d42306d
--- /dev/null
+++ b/server/libs/walls/walls.go
@@ -0,0 +1,17 @@
+package walls
+
+import (
+ "net/http"
+)
+
+type Walls interface {
+ PassIpLimit(remoteAddr string) bool
+ PassOpLimit(resourceId string, opId int16) bool
+ PassLoginCheck(tokenStr string, req *http.Request) bool
+ MakeLoginToken(uid string) string
+}
+
+type LoginToken struct {
+ AdminId string
+ Expires int64
+}
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000..c74c478
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,6993 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@types/node@*":
+ version "10.1.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.1.2.tgz#1b928a0baa408fc8ae3ac012cc81375addc147c6"
+
+abab@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+
+accepts@~1.3.4, accepts@~1.3.5:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
+ dependencies:
+ mime-types "~2.1.18"
+ negotiator "0.6.1"
+
+acorn-dynamic-import@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
+ dependencies:
+ acorn "^4.0.3"
+
+acorn-globals@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"
+ dependencies:
+ acorn "^4.0.4"
+
+acorn-jsx@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+ dependencies:
+ acorn "^3.0.4"
+
+acorn@^3.0.4:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+
+acorn@^4.0.3, acorn@^4.0.4:
+ version "4.0.13"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
+
+acorn@^5.0.0, acorn@^5.5.0:
+ version "5.5.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
+
+ajv-keywords@^1.1.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
+
+ajv-keywords@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
+
+ajv-keywords@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
+
+ajv@^4.7.0:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
+ version "5.5.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.3.0"
+
+ajv@^6.1.0:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.0.tgz#4c8affdf80887d8f132c9c52ab8a2dc4d0b7b24c"
+ dependencies:
+ fast-deep-equal "^2.0.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.3.0"
+ uri-js "^4.2.1"
+
+align-text@^0.1.1, align-text@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
+ dependencies:
+ kind-of "^3.0.2"
+ longest "^1.0.1"
+ repeat-string "^1.5.2"
+
+alphanum-sort@^1.0.1, alphanum-sort@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+
+amdefine@>=0.0.4:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
+
+ansi-escapes@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30"
+
+ansi-html@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+append-transform@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
+ dependencies:
+ default-require-extensions "^1.0.0"
+
+aproba@^1.0.3, aproba@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+
+are-we-there-yet@~1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+argparse@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+ dependencies:
+ sprintf-js "~1.0.2"
+
+aria-query@^0.7.0:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-0.7.1.tgz#26cbb5aff64144b0a825be1846e0b16cfa00b11e"
+ dependencies:
+ ast-types-flow "0.0.7"
+ commander "^2.11.0"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ 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"
+
+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"
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+
+array-find-index@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+
+array-flatten@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296"
+
+array-includes@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.7.0"
+
+array-union@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+ dependencies:
+ array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+
+arrify@^1.0.0, arrify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+asap@~2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+
+asn1.js@^4.0.0:
+ version "4.10.1"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+
+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"
+
+assert@^1.1.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+ dependencies:
+ util "0.10.3"
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+
+ast-types-flow@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
+
+astral-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+
+async-each@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+
+async@^1.4.0, async@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+
+async@^2.1.2, async@^2.1.4:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
+ dependencies:
+ lodash "^4.17.10"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+atob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a"
+
+autoprefixer@^6.3.1:
+ version "6.7.7"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"
+ dependencies:
+ browserslist "^1.7.6"
+ caniuse-db "^1.0.30000634"
+ normalize-range "^0.1.2"
+ num2fraction "^1.2.2"
+ postcss "^5.2.16"
+ postcss-value-parser "^3.2.3"
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+
+aws4@^1.6.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289"
+
+axios@^0.16.2:
+ version "0.16.2"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d"
+ dependencies:
+ follow-redirects "^1.2.3"
+ is-buffer "^1.1.5"
+
+axobject-query@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-0.1.0.tgz#62f59dbc59c9f9242759ca349960e7a2fe3c36c0"
+ dependencies:
+ ast-types-flow "0.0.7"
+
+babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+ dependencies:
+ chalk "^1.1.3"
+ esutils "^2.0.2"
+ js-tokens "^3.0.2"
+
+babel-core@^6.0.0, babel-core@^6.26.0:
+ version "6.26.3"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-generator "^6.26.0"
+ babel-helpers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-register "^6.26.0"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ convert-source-map "^1.5.1"
+ debug "^2.6.9"
+ json5 "^0.5.1"
+ lodash "^4.17.4"
+ minimatch "^3.0.4"
+ path-is-absolute "^1.0.1"
+ private "^0.1.8"
+ slash "^1.0.0"
+ source-map "^0.5.7"
+
+babel-generator@^6.18.0, babel-generator@^6.26.0:
+ version "6.26.1"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.17.4"
+ source-map "^0.5.7"
+ trim-right "^1.0.1"
+
+babel-helper-bindify-decorators@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664"
+ dependencies:
+ babel-helper-explode-assignable-expression "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-builder-react-jsx@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz#39ff8313b75c8b65dceff1f31d383e0ff2a408a0"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ esutils "^2.0.2"
+
+babel-helper-call-delegate@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-define-map@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-helper-explode-assignable-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-explode-class@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb"
+ dependencies:
+ babel-helper-bindify-decorators "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
+ dependencies:
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-get-function-arity@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-hoist-variables@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-optimise-call-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-regex@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-helper-remap-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-replace-supers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
+ dependencies:
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-jest@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-21.2.0.tgz#2ce059519a9374a2c46f2455b6fbef5ad75d863e"
+ dependencies:
+ babel-plugin-istanbul "^4.0.0"
+ babel-preset-jest "^21.2.0"
+
+babel-loader@^6.4.1:
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-6.4.1.tgz#0b34112d5b0748a8dcdbf51acf6f9bd42d50b8ca"
+ dependencies:
+ find-cache-dir "^0.1.1"
+ loader-utils "^0.2.16"
+ mkdirp "^0.5.1"
+ object-assign "^4.0.1"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-check-es2015-constants@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-istanbul@^4.0.0:
+ version "4.1.6"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45"
+ dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.13.0"
+ find-up "^2.1.0"
+ istanbul-lib-instrument "^1.10.1"
+ test-exclude "^4.2.1"
+
+babel-plugin-jest-hoist@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-21.2.0.tgz#2cef637259bd4b628a6cace039de5fcd14dbb006"
+
+babel-plugin-syntax-async-functions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
+
+babel-plugin-syntax-async-generators@^6.5.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a"
+
+babel-plugin-syntax-class-constructor-call@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416"
+
+babel-plugin-syntax-class-properties@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
+
+babel-plugin-syntax-decorators@^6.13.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b"
+
+babel-plugin-syntax-do-expressions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz#5747756139aa26d390d09410b03744ba07e4796d"
+
+babel-plugin-syntax-dynamic-import@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da"
+
+babel-plugin-syntax-exponentiation-operator@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
+
+babel-plugin-syntax-export-extensions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721"
+
+babel-plugin-syntax-flow@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d"
+
+babel-plugin-syntax-function-bind@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz#48c495f177bdf31a981e732f55adc0bdd2601f46"
+
+babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+
+babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
+
+babel-plugin-syntax-trailing-function-commas@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
+
+babel-plugin-transform-async-generator-functions@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.24.1"
+ babel-plugin-syntax-async-generators "^6.5.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-async-to-generator@^6.22.0, babel-plugin-transform-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.24.1"
+ babel-plugin-syntax-async-functions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-class-constructor-call@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9"
+ dependencies:
+ babel-plugin-syntax-class-constructor-call "^6.18.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-class-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-plugin-syntax-class-properties "^6.8.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-decorators@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d"
+ dependencies:
+ babel-helper-explode-class "^6.24.1"
+ babel-plugin-syntax-decorators "^6.13.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-do-expressions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz#28ccaf92812d949c2cd1281f690c8fdc468ae9bb"
+ dependencies:
+ babel-plugin-syntax-do-expressions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-arrow-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoping@^6.23.0, babel-plugin-transform-es2015-block-scoping@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-plugin-transform-es2015-classes@^6.23.0, babel-plugin-transform-es2015-classes@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
+ dependencies:
+ babel-helper-define-map "^6.24.1"
+ babel-helper-function-name "^6.24.1"
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-helper-replace-supers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-computed-properties@^6.22.0, babel-plugin-transform-es2015-computed-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-destructuring@^6.22.0, babel-plugin-transform-es2015-destructuring@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-duplicate-keys@^6.22.0, babel-plugin-transform-es2015-duplicate-keys@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-for-of@^6.22.0, babel-plugin-transform-es2015-for-of@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-function-name@^6.22.0, babel-plugin-transform-es2015-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
+ dependencies:
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
+ version "6.26.2"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
+ dependencies:
+ babel-plugin-transform-strict-mode "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-types "^6.26.0"
+
+babel-plugin-transform-es2015-modules-systemjs@^6.23.0, babel-plugin-transform-es2015-modules-systemjs@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-umd@^6.23.0, babel-plugin-transform-es2015-modules-umd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
+ dependencies:
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-object-super@^6.22.0, babel-plugin-transform-es2015-object-super@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
+ dependencies:
+ babel-helper-replace-supers "^6.24.1"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-parameters@^6.23.0, babel-plugin-transform-es2015-parameters@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
+ dependencies:
+ babel-helper-call-delegate "^6.24.1"
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-shorthand-properties@^6.22.0, babel-plugin-transform-es2015-shorthand-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-spread@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-sticky-regex@^6.22.0, babel-plugin-transform-es2015-sticky-regex@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-template-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-typeof-symbol@^6.22.0, babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-unicode-regex@^6.22.0, babel-plugin-transform-es2015-unicode-regex@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ regexpu-core "^2.0.0"
+
+babel-plugin-transform-exponentiation-operator@^6.22.0, babel-plugin-transform-exponentiation-operator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
+ dependencies:
+ babel-helper-builder-binary-assignment-operator-visitor "^6.24.1"
+ babel-plugin-syntax-exponentiation-operator "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-export-extensions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653"
+ dependencies:
+ babel-plugin-syntax-export-extensions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-flow-strip-types@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf"
+ dependencies:
+ babel-plugin-syntax-flow "^6.18.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-function-bind@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz#c6fb8e96ac296a310b8cf8ea401462407ddf6a97"
+ dependencies:
+ babel-plugin-syntax-function-bind "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-object-rest-spread@^6.22.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
+ dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.8.0"
+ babel-runtime "^6.26.0"
+
+babel-plugin-transform-react-display-name@^6.23.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx-self@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx-source@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz#66ac12153f5cd2d17b3c19268f4bf0197f44ecd6"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz#840a028e7df460dfc3a2d29f0c0d91f6376e66a3"
+ dependencies:
+ babel-helper-builder-react-jsx "^6.24.1"
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-regenerator@^6.22.0, babel-plugin-transform-regenerator@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
+ dependencies:
+ regenerator-transform "^0.10.0"
+
+babel-plugin-transform-strict-mode@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-preset-env@^1.2.2:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a"
+ dependencies:
+ babel-plugin-check-es2015-constants "^6.22.0"
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.22.0"
+ babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.23.0"
+ babel-plugin-transform-es2015-classes "^6.23.0"
+ babel-plugin-transform-es2015-computed-properties "^6.22.0"
+ babel-plugin-transform-es2015-destructuring "^6.23.0"
+ babel-plugin-transform-es2015-duplicate-keys "^6.22.0"
+ babel-plugin-transform-es2015-for-of "^6.23.0"
+ babel-plugin-transform-es2015-function-name "^6.22.0"
+ babel-plugin-transform-es2015-literals "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-systemjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-umd "^6.23.0"
+ babel-plugin-transform-es2015-object-super "^6.22.0"
+ babel-plugin-transform-es2015-parameters "^6.23.0"
+ babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
+ babel-plugin-transform-es2015-spread "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.22.0"
+ babel-plugin-transform-es2015-template-literals "^6.22.0"
+ babel-plugin-transform-es2015-typeof-symbol "^6.23.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.22.0"
+ babel-plugin-transform-exponentiation-operator "^6.22.0"
+ babel-plugin-transform-regenerator "^6.22.0"
+ browserslist "^3.2.6"
+ invariant "^2.2.2"
+ semver "^5.3.0"
+
+babel-preset-es2015@^6.24.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939"
+ dependencies:
+ babel-plugin-check-es2015-constants "^6.22.0"
+ babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.24.1"
+ babel-plugin-transform-es2015-classes "^6.24.1"
+ babel-plugin-transform-es2015-computed-properties "^6.24.1"
+ babel-plugin-transform-es2015-destructuring "^6.22.0"
+ babel-plugin-transform-es2015-duplicate-keys "^6.24.1"
+ babel-plugin-transform-es2015-for-of "^6.22.0"
+ babel-plugin-transform-es2015-function-name "^6.24.1"
+ babel-plugin-transform-es2015-literals "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-plugin-transform-es2015-modules-systemjs "^6.24.1"
+ babel-plugin-transform-es2015-modules-umd "^6.24.1"
+ babel-plugin-transform-es2015-object-super "^6.24.1"
+ babel-plugin-transform-es2015-parameters "^6.24.1"
+ babel-plugin-transform-es2015-shorthand-properties "^6.24.1"
+ babel-plugin-transform-es2015-spread "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.24.1"
+ babel-plugin-transform-es2015-template-literals "^6.22.0"
+ babel-plugin-transform-es2015-typeof-symbol "^6.22.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.24.1"
+ babel-plugin-transform-regenerator "^6.24.1"
+
+babel-preset-flow@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz#e71218887085ae9a24b5be4169affb599816c49d"
+ dependencies:
+ babel-plugin-transform-flow-strip-types "^6.22.0"
+
+babel-preset-jest@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-21.2.0.tgz#ff9d2bce08abd98e8a36d9a8a5189b9173b85638"
+ dependencies:
+ babel-plugin-jest-hoist "^21.2.0"
+ babel-plugin-syntax-object-rest-spread "^6.13.0"
+
+babel-preset-react@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.24.1.tgz#ba69dfaea45fc3ec639b6a4ecea6e17702c91380"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.3.13"
+ babel-plugin-transform-react-display-name "^6.23.0"
+ babel-plugin-transform-react-jsx "^6.24.1"
+ babel-plugin-transform-react-jsx-self "^6.22.0"
+ babel-plugin-transform-react-jsx-source "^6.22.0"
+ babel-preset-flow "^6.23.0"
+
+babel-preset-stage-0@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz#5642d15042f91384d7e5af8bc88b1db95b039e6a"
+ dependencies:
+ babel-plugin-transform-do-expressions "^6.22.0"
+ babel-plugin-transform-function-bind "^6.22.0"
+ babel-preset-stage-1 "^6.24.1"
+
+babel-preset-stage-1@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0"
+ dependencies:
+ babel-plugin-transform-class-constructor-call "^6.24.1"
+ babel-plugin-transform-export-extensions "^6.22.0"
+ babel-preset-stage-2 "^6.24.1"
+
+babel-preset-stage-2@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1"
+ dependencies:
+ babel-plugin-syntax-dynamic-import "^6.18.0"
+ babel-plugin-transform-class-properties "^6.24.1"
+ babel-plugin-transform-decorators "^6.24.1"
+ babel-preset-stage-3 "^6.24.1"
+
+babel-preset-stage-3@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395"
+ dependencies:
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-generator-functions "^6.24.1"
+ babel-plugin-transform-async-to-generator "^6.24.1"
+ babel-plugin-transform-exponentiation-operator "^6.24.1"
+ babel-plugin-transform-object-rest-spread "^6.22.0"
+
+babel-register@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
+ dependencies:
+ babel-core "^6.26.0"
+ babel-runtime "^6.26.0"
+ core-js "^2.5.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.17.4"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.15"
+
+babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.11.0"
+
+babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ lodash "^4.17.4"
+
+babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ debug "^2.6.8"
+ globals "^9.18.0"
+ invariant "^2.2.2"
+ lodash "^4.17.4"
+
+babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
+ dependencies:
+ babel-runtime "^6.26.0"
+ esutils "^2.0.2"
+ lodash "^4.17.4"
+ to-fast-properties "^1.0.3"
+
+babylon@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+
+balanced-match@^0.4.2:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+base64-js@^1.0.2:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
+
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
+batch@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+big.js@^3.1.3:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
+
+binary-extensions@^1.0.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
+
+bluebird@^3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+
+body-parser@1.18.2:
+ version "1.18.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
+ dependencies:
+ bytes "3.0.0"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.1"
+ http-errors "~1.6.2"
+ iconv-lite "0.4.19"
+ on-finished "~2.3.0"
+ qs "6.5.1"
+ raw-body "2.3.2"
+ type-is "~1.6.15"
+
+bonjour@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
+ dependencies:
+ array-flatten "^2.1.0"
+ deep-equal "^1.0.1"
+ dns-equal "^1.0.0"
+ dns-txt "^2.0.2"
+ multicast-dns "^6.0.1"
+ multicast-dns-service-types "^1.1.0"
+
+boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+braces@^2.3.0, braces@^2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+brorand@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+
+browser-resolve@^1.11.2:
+ version "1.11.2"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce"
+ dependencies:
+ resolve "1.1.7"
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
+ dependencies:
+ buffer-xor "^1.0.3"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.3"
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0"
+ dependencies:
+ browserify-aes "^1.0.4"
+ browserify-des "^1.0.0"
+ evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.1.tgz#3343124db6d7ad53e26a8826318712bdc8450f9c"
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+
+browserify-rsa@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+ dependencies:
+ bn.js "^4.1.0"
+ randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+ dependencies:
+ bn.js "^4.1.1"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.2"
+ elliptic "^6.0.0"
+ inherits "^2.0.1"
+ parse-asn1 "^5.0.0"
+
+browserify-zlib@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+ dependencies:
+ pako "~1.0.5"
+
+browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
+ version "1.7.7"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9"
+ dependencies:
+ caniuse-db "^1.0.30000639"
+ electron-to-chromium "^1.2.7"
+
+browserslist@^3.2.6:
+ version "3.2.8"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6"
+ dependencies:
+ caniuse-lite "^1.0.30000844"
+ electron-to-chromium "^1.3.47"
+
+bser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
+ dependencies:
+ node-int64 "^0.4.0"
+
+buffer-from@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531"
+
+buffer-indexof@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c"
+
+buffer-xor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+
+buffer@^4.3.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+builtin-status-codes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+
+byte-size@^4.0.2:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-4.0.3.tgz#b7c095efc68eadf82985fccd9a2df43a74fa2ccd"
+
+bytes@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+
+cacache@^10.0.4:
+ version "10.0.4"
+ resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
+ dependencies:
+ bluebird "^3.5.1"
+ chownr "^1.0.1"
+ glob "^7.1.2"
+ graceful-fs "^4.1.11"
+ lru-cache "^4.1.1"
+ mississippi "^2.0.0"
+ mkdirp "^0.5.1"
+ move-concurrently "^1.0.1"
+ promise-inflight "^1.0.1"
+ rimraf "^2.6.2"
+ ssri "^5.2.4"
+ unique-filename "^1.1.0"
+ y18n "^4.0.0"
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
+caller-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
+ dependencies:
+ callsites "^0.2.0"
+
+callsites@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+
+callsites@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+
+camel-case@3.0.x:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
+ dependencies:
+ no-case "^2.2.0"
+ upper-case "^1.1.1"
+
+camelcase-keys@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+ dependencies:
+ camelcase "^2.0.0"
+ map-obj "^1.0.0"
+
+camelcase@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
+
+camelcase@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+
+camelcase@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+
+camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+
+caniuse-api@^1.5.2:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c"
+ dependencies:
+ browserslist "^1.3.6"
+ caniuse-db "^1.0.30000529"
+ lodash.memoize "^4.1.2"
+ lodash.uniq "^4.5.0"
+
+caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
+ version "1.0.30000846"
+ resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000846.tgz#d9c86f914738db4da098eeded997413c44561bd2"
+
+caniuse-lite@^1.0.30000844:
+ version "1.0.30000844"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000844.tgz#de7c84cde0582143cf4f5abdf1b98e5a0539ad4a"
+
+capture-exit@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f"
+ dependencies:
+ rsvp "^3.3.3"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+center-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
+ dependencies:
+ align-text "^0.1.3"
+ lazy-cache "^1.0.3"
+
+chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chardet@^0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
+
+charenc@~0.0.1:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
+
+cheerio@^1.0.0-rc.2:
+ version "1.0.0-rc.2"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
+ dependencies:
+ css-select "~1.2.0"
+ dom-serializer "~0.1.0"
+ entities "~1.1.1"
+ htmlparser2 "^3.9.1"
+ lodash "^4.15.0"
+ parse5 "^3.0.1"
+
+chokidar@^2.0.0, chokidar@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176"
+ dependencies:
+ anymatch "^2.0.0"
+ async-each "^1.0.0"
+ braces "^2.3.0"
+ glob-parent "^3.1.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^4.0.0"
+ normalize-path "^2.1.1"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ upath "^1.0.0"
+ optionalDependencies:
+ fsevents "^1.1.2"
+
+chownr@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
+
+ci-info@^1.0.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.3.tgz#710193264bb05c77b8c90d02f5aaf22216a667b2"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+circular-json@^0.3.1:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
+
+clap@^1.0.9:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51"
+ dependencies:
+ chalk "^1.1.3"
+
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+clean-css@4.1.x:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.11.tgz#2ecdf145aba38f54740f26cefd0ff3e03e125d6a"
+ dependencies:
+ source-map "0.5.x"
+
+clean-webpack-plugin@^0.1.19:
+ version "0.1.19"
+ resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-0.1.19.tgz#ceda8bb96b00fe168e9b080272960d20fdcadd6d"
+ dependencies:
+ rimraf "^2.6.1"
+
+cli-cursor@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+ dependencies:
+ restore-cursor "^2.0.0"
+
+cli-width@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
+
+cliui@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
+ dependencies:
+ center-align "^0.1.1"
+ right-align "^0.1.1"
+ wordwrap "0.0.2"
+
+cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+clone@^1.0.2:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+coa@~1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd"
+ dependencies:
+ q "^1.1.2"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
+color-convert@^1.3.0, color-convert@^1.9.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
+ dependencies:
+ color-name "^1.1.1"
+
+color-name@^1.0.0, color-name@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+color-string@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991"
+ dependencies:
+ color-name "^1.0.0"
+
+color@^0.11.0:
+ version "0.11.4"
+ resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764"
+ dependencies:
+ clone "^1.0.2"
+ color-convert "^1.3.0"
+ color-string "^0.3.0"
+
+colormin@^1.0.5:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133"
+ dependencies:
+ color "^0.11.0"
+ css-color-names "0.0.4"
+ has "^1.0.1"
+
+colors@0.5.x:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-0.5.1.tgz#7d0023eaeb154e8ee9fce75dcb923d0ed1667774"
+
+colors@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
+
+combined-stream@1.0.6, combined-stream@~1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@2.15.x, commander@^2.11.0, commander@~2.15.0:
+ version "2.15.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
+
+commander@~2.13.0:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
+
+commondir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+
+compare-versions@^3.1.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.2.1.tgz#a49eb7689d4caaf0b6db5220173fd279614000f7"
+
+component-emitter@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+
+compressible@~2.0.13:
+ version "2.0.13"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.13.tgz#0d1020ab924b2fdb4d6279875c7d6daba6baa7a9"
+ dependencies:
+ mime-db ">= 1.33.0 < 2"
+
+compression@^1.5.2:
+ version "1.7.2"
+ resolved "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz#aaffbcd6aaf854b44ebb280353d5ad1651f59a69"
+ dependencies:
+ accepts "~1.3.4"
+ bytes "3.0.0"
+ compressible "~2.0.13"
+ debug "2.6.9"
+ on-headers "~1.0.1"
+ safe-buffer "5.1.1"
+ vary "~1.1.2"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+concat-stream@^1.5.0, concat-stream@^1.6.0:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+ dependencies:
+ buffer-from "^1.0.0"
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+connect-history-api-fallback@^1.3.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a"
+
+console-browserify@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
+ dependencies:
+ date-now "^0.1.4"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+
+constants-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+
+contains-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+
+content-disposition@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
+
+content-type-parser@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7"
+
+content-type@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+
+convert-source-map@^1.4.0, convert-source-map@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+
+cookie@0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
+
+copy-concurrently@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
+ dependencies:
+ aproba "^1.1.1"
+ fs-write-stream-atomic "^1.0.8"
+ iferr "^0.1.5"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.0"
+
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+
+core-js@^1.0.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+
+core-js@^2.4.0, core-js@^2.5.0:
+ version "2.5.6"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.6.tgz#0fe6d45bf3cac3ac364a9d72de7576f4eb221b9d"
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+create-ecdh@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff"
+ dependencies:
+ bn.js "^4.1.0"
+ elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ md5.js "^1.3.4"
+ ripemd160 "^2.0.1"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+create-react-class@^15.6.0:
+ version "15.6.3"
+ resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036"
+ dependencies:
+ fbjs "^0.8.9"
+ loose-envify "^1.3.1"
+ object-assign "^4.1.1"
+
+cross-spawn@^5.0.1, cross-spawn@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+crypt@~0.0.1:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
+
+crypto-browserify@^3.11.0:
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
+ dependencies:
+ browserify-cipher "^1.0.0"
+ browserify-sign "^4.0.0"
+ create-ecdh "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.0"
+ diffie-hellman "^5.0.0"
+ inherits "^2.0.1"
+ pbkdf2 "^3.0.3"
+ public-encrypt "^4.0.0"
+ randombytes "^2.0.0"
+ randomfill "^1.0.3"
+
+css-color-names@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
+
+css-loader@^0.28.11:
+ version "0.28.11"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ css-selector-tokenizer "^0.7.0"
+ cssnano "^3.10.0"
+ icss-utils "^2.1.0"
+ loader-utils "^1.0.2"
+ lodash.camelcase "^4.3.0"
+ object-assign "^4.1.1"
+ postcss "^5.0.6"
+ postcss-modules-extract-imports "^1.2.0"
+ postcss-modules-local-by-default "^1.2.0"
+ postcss-modules-scope "^1.1.0"
+ postcss-modules-values "^1.3.0"
+ postcss-value-parser "^3.3.0"
+ source-list-map "^2.0.0"
+
+css-select@^1.1.0, css-select@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "~1.0.1"
+
+css-selector-tokenizer@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86"
+ dependencies:
+ cssesc "^0.1.0"
+ fastparse "^1.1.1"
+ regexpu-core "^1.0.0"
+
+css-what@2.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
+
+cssesc@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
+
+cssnano@^3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38"
+ dependencies:
+ autoprefixer "^6.3.1"
+ decamelize "^1.1.2"
+ defined "^1.0.0"
+ has "^1.0.1"
+ object-assign "^4.0.1"
+ postcss "^5.0.14"
+ postcss-calc "^5.2.0"
+ postcss-colormin "^2.1.8"
+ postcss-convert-values "^2.3.4"
+ postcss-discard-comments "^2.0.4"
+ postcss-discard-duplicates "^2.0.1"
+ postcss-discard-empty "^2.0.1"
+ postcss-discard-overridden "^0.1.1"
+ postcss-discard-unused "^2.2.1"
+ postcss-filter-plugins "^2.0.0"
+ postcss-merge-idents "^2.1.5"
+ postcss-merge-longhand "^2.0.1"
+ postcss-merge-rules "^2.0.3"
+ postcss-minify-font-values "^1.0.2"
+ postcss-minify-gradients "^1.0.1"
+ postcss-minify-params "^1.0.4"
+ postcss-minify-selectors "^2.0.4"
+ postcss-normalize-charset "^1.1.0"
+ postcss-normalize-url "^3.0.7"
+ postcss-ordered-values "^2.1.0"
+ postcss-reduce-idents "^2.2.2"
+ postcss-reduce-initial "^1.0.0"
+ postcss-reduce-transforms "^1.0.3"
+ postcss-svgo "^2.1.1"
+ postcss-unique-selectors "^2.0.2"
+ postcss-value-parser "^3.2.3"
+ postcss-zindex "^2.0.1"
+
+csso@~2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85"
+ dependencies:
+ clap "^1.0.9"
+ source-map "^0.5.3"
+
+cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b"
+
+"cssstyle@>= 0.2.37 < 0.3.0":
+ version "0.2.37"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54"
+ dependencies:
+ cssom "0.3.x"
+
+currently-unhandled@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+ dependencies:
+ array-find-index "^1.0.1"
+
+cyclist@~0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
+
+damerau-levenshtein@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ dependencies:
+ assert-plus "^1.0.0"
+
+date-now@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+
+debounce@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408"
+
+debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+ dependencies:
+ ms "2.0.0"
+
+decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+decode-uri-component@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+
+deep-equal@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
+
+deep-extend@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+default-require-extensions@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
+ dependencies:
+ strip-bom "^2.0.0"
+
+define-properties@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
+ dependencies:
+ foreach "^2.0.5"
+ object-keys "^1.0.8"
+
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
+defined@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+
+del@^2.0.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
+ dependencies:
+ globby "^5.0.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ rimraf "^2.2.8"
+
+del@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5"
+ dependencies:
+ globby "^6.1.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ p-map "^1.1.1"
+ pify "^3.0.0"
+ rimraf "^2.2.8"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+
+depd@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
+
+depd@~1.1.1, depd@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+
+des.js@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
+ dependencies:
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+destroy@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ dependencies:
+ repeating "^2.0.0"
+
+detect-libc@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+
+detect-node@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127"
+
+diff@^3.2.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
+
+diffie-hellman@^5.0.0:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
+ dependencies:
+ bn.js "^4.1.0"
+ miller-rabin "^4.0.0"
+ randombytes "^2.0.0"
+
+discontinuous-range@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+
+dns-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
+
+dns-packet@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a"
+ dependencies:
+ ip "^1.1.0"
+ safe-buffer "^5.0.1"
+
+dns-txt@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6"
+ dependencies:
+ buffer-indexof "^1.0.0"
+
+doctrine@1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.0"
+
+doctrine@^2.0.2, doctrine@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
+ dependencies:
+ esutils "^2.0.2"
+
+dom-converter@~0.1:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b"
+ dependencies:
+ utila "~0.3"
+
+dom-serializer@0, dom-serializer@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
+ dependencies:
+ domelementtype "~1.1.1"
+ entities "~1.1.1"
+
+domain-browser@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
+
+domelementtype@1, domelementtype@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
+
+domelementtype@~1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
+
+domhandler@2.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594"
+ dependencies:
+ domelementtype "1"
+
+domhandler@^2.3.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+ dependencies:
+ domelementtype "1"
+
+domutils@1.1:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+domutils@^1.5.1:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+duplexify@^3.4.2, duplexify@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.0.tgz#592903f5d80b38d037220541264d69a198fb3410"
+ dependencies:
+ end-of-stream "^1.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+ stream-shift "^1.0.0"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
+ dependencies:
+ jsbn "~0.1.0"
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+
+electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.47:
+ version "1.3.48"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz#d3b0d8593814044e092ece2108fc3ac9aea4b900"
+
+elliptic@^6.0.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
+ dependencies:
+ bn.js "^4.4.0"
+ brorand "^1.0.1"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.0"
+
+emoji-regex@^6.1.0:
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"
+
+emojis-list@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+
+encodeurl@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+
+encoding@^0.1.11:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
+ dependencies:
+ iconv-lite "~0.4.13"
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
+ dependencies:
+ once "^1.4.0"
+
+enhanced-resolve@^3.3.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.4.0"
+ object-assign "^4.0.1"
+ tapable "^0.2.7"
+
+entities@^1.1.1, entities@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
+
+enzyme-adapter-react-15@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-15/-/enzyme-adapter-react-15-1.0.5.tgz#99f9a03ff2c2303e517342935798a6bdfbb75fac"
+ dependencies:
+ enzyme-adapter-utils "^1.1.0"
+ lodash "^4.17.4"
+ object.assign "^4.0.4"
+ object.values "^1.0.4"
+ prop-types "^15.5.10"
+
+enzyme-adapter-utils@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz#d6c85756826c257a8544d362cc7a67e97ea698c7"
+ dependencies:
+ lodash "^4.17.4"
+ object.assign "^4.0.4"
+ prop-types "^15.6.0"
+
+enzyme@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479"
+ dependencies:
+ cheerio "^1.0.0-rc.2"
+ function.prototype.name "^1.0.3"
+ has "^1.0.1"
+ is-boolean-object "^1.0.0"
+ is-callable "^1.1.3"
+ is-number-object "^1.0.3"
+ is-string "^1.0.4"
+ is-subset "^0.1.1"
+ lodash "^4.17.4"
+ object-inspect "^1.5.0"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.0.4"
+ object.values "^1.0.4"
+ raf "^3.4.0"
+ rst-selector-parser "^2.2.3"
+
+errno@^0.1.3, errno@~0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
+ dependencies:
+ prr "~1.0.1"
+
+error-ex@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.11.0.tgz#cce87d518f0496893b1a30cd8461835535480681"
+ dependencies:
+ es-to-primitive "^1.1.1"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+ is-callable "^1.1.3"
+ is-regex "^1.0.4"
+
+es-to-primitive@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d"
+ dependencies:
+ is-callable "^1.1.1"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.1"
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+escodegen@^1.6.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2"
+ dependencies:
+ esprima "^3.1.3"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+eslint-config-airbnb-base@^12.1.0:
+ version "12.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz#386441e54a12ccd957b0a92564a4bafebd747944"
+ dependencies:
+ eslint-restricted-globals "^0.1.1"
+
+eslint-config-airbnb@^16.0.0:
+ version "16.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-16.1.0.tgz#2546bfb02cc9fe92284bf1723ccf2e87bc45ca46"
+ dependencies:
+ eslint-config-airbnb-base "^12.1.0"
+
+eslint-import-resolver-node@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
+ dependencies:
+ debug "^2.6.9"
+ resolve "^1.5.0"
+
+eslint-module-utils@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746"
+ dependencies:
+ debug "^2.6.8"
+ pkg-dir "^1.0.0"
+
+eslint-plugin-import@^2.7.0:
+ version "2.12.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.12.0.tgz#dad31781292d6664b25317fd049d2e2b2f02205d"
+ dependencies:
+ contains-path "^0.1.0"
+ debug "^2.6.8"
+ doctrine "1.5.0"
+ eslint-import-resolver-node "^0.3.1"
+ eslint-module-utils "^2.2.0"
+ has "^1.0.1"
+ lodash "^4.17.4"
+ minimatch "^3.0.3"
+ read-pkg-up "^2.0.0"
+ resolve "^1.6.0"
+
+eslint-plugin-jsx-a11y@^6.0.2:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.0.3.tgz#54583d1ae442483162e040e13cc31865465100e5"
+ dependencies:
+ aria-query "^0.7.0"
+ array-includes "^3.0.3"
+ ast-types-flow "0.0.7"
+ axobject-query "^0.1.0"
+ damerau-levenshtein "^1.0.0"
+ emoji-regex "^6.1.0"
+ jsx-ast-utils "^2.0.0"
+
+eslint-plugin-react@^7.4.0:
+ version "7.8.2"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.8.2.tgz#e95c9c47fece55d2303d1a67c9d01b930b88a51d"
+ dependencies:
+ doctrine "^2.0.2"
+ has "^1.0.1"
+ jsx-ast-utils "^2.0.1"
+ prop-types "^15.6.0"
+
+eslint-restricted-globals@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7"
+
+eslint-scope@^3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-visitor-keys@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
+
+eslint@^4.12.0:
+ version "4.19.1"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300"
+ dependencies:
+ ajv "^5.3.0"
+ babel-code-frame "^6.22.0"
+ chalk "^2.1.0"
+ concat-stream "^1.6.0"
+ cross-spawn "^5.1.0"
+ debug "^3.1.0"
+ doctrine "^2.1.0"
+ eslint-scope "^3.7.1"
+ eslint-visitor-keys "^1.0.0"
+ espree "^3.5.4"
+ esquery "^1.0.0"
+ esutils "^2.0.2"
+ file-entry-cache "^2.0.0"
+ functional-red-black-tree "^1.0.1"
+ glob "^7.1.2"
+ globals "^11.0.1"
+ ignore "^3.3.3"
+ imurmurhash "^0.1.4"
+ inquirer "^3.0.6"
+ is-resolvable "^1.0.0"
+ js-yaml "^3.9.1"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ levn "^0.3.0"
+ lodash "^4.17.4"
+ minimatch "^3.0.2"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ optionator "^0.8.2"
+ path-is-inside "^1.0.2"
+ pluralize "^7.0.0"
+ progress "^2.0.0"
+ regexpp "^1.0.1"
+ require-uncached "^1.0.3"
+ semver "^5.3.0"
+ strip-ansi "^4.0.0"
+ strip-json-comments "~2.0.1"
+ table "4.0.2"
+ text-table "~0.2.0"
+
+espree@^3.5.4:
+ version "3.5.4"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
+ dependencies:
+ acorn "^5.5.0"
+ acorn-jsx "^3.0.0"
+
+esprima@^2.6.0:
+ version "2.7.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
+
+esprima@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+
+esquery@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
+ dependencies:
+ estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
+ dependencies:
+ estraverse "^4.1.0"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+
+eventemitter3@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
+
+events@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+
+eventsource@0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232"
+ dependencies:
+ original ">=0.0.5"
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+ dependencies:
+ md5.js "^1.3.4"
+ safe-buffer "^5.1.1"
+
+exec-sh@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38"
+ dependencies:
+ merge "^1.1.3"
+
+execa@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
+ dependencies:
+ cross-spawn "^5.0.1"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ dependencies:
+ fill-range "^2.1.0"
+
+expect@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-21.2.1.tgz#003ac2ac7005c3c29e73b38a272d4afadd6d1d7b"
+ dependencies:
+ ansi-styles "^3.2.0"
+ jest-diff "^21.2.1"
+ jest-get-type "^21.2.0"
+ jest-matcher-utils "^21.2.1"
+ jest-message-util "^21.2.1"
+ jest-regex-util "^21.2.0"
+
+express@^4.16.2:
+ version "4.16.3"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
+ dependencies:
+ accepts "~1.3.5"
+ array-flatten "1.1.1"
+ body-parser "1.18.2"
+ content-disposition "0.5.2"
+ content-type "~1.0.4"
+ cookie "0.3.1"
+ cookie-signature "1.0.6"
+ debug "2.6.9"
+ depd "~1.1.2"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ finalhandler "1.1.1"
+ fresh "0.5.2"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "~2.3.0"
+ parseurl "~1.3.2"
+ path-to-regexp "0.1.7"
+ proxy-addr "~2.0.3"
+ qs "6.5.1"
+ range-parser "~1.2.0"
+ safe-buffer "5.1.1"
+ send "0.16.2"
+ serve-static "1.13.2"
+ setprototypeof "1.1.0"
+ statuses "~1.4.0"
+ type-is "~1.6.16"
+ utils-merge "1.0.1"
+ vary "~1.1.2"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
+extend@~3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+
+external-editor@^2.0.4:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
+ dependencies:
+ chardet "^0.4.0"
+ iconv-lite "^0.4.17"
+ tmp "^0.0.33"
+
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ dependencies:
+ is-extglob "^1.0.0"
+
+extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+
+fast-deep-equal@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
+
+fast-deep-equal@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+fastparse@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
+
+faye-websocket@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+faye-websocket@~0.11.0:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38"
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+fb-watchman@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+ dependencies:
+ bser "^2.0.0"
+
+fbjs@^0.8.16, fbjs@^0.8.9:
+ version "0.8.16"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
+ dependencies:
+ core-js "^1.0.0"
+ isomorphic-fetch "^2.1.1"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.9"
+
+figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+file-entry-cache@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
+ dependencies:
+ flat-cache "^1.2.1"
+ object-assign "^4.0.1"
+
+file-loader@^1.1.11:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8"
+ dependencies:
+ loader-utils "^1.0.2"
+ schema-utils "^0.4.5"
+
+filename-regex@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+
+fileset@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0"
+ dependencies:
+ glob "^7.0.3"
+ minimatch "^3.0.3"
+
+fill-range@^2.1.0:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565"
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^3.0.0"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
+finalhandler@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.2"
+ statuses "~1.4.0"
+ unpipe "~1.0.0"
+
+find-cache-dir@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9"
+ dependencies:
+ commondir "^1.0.1"
+ mkdirp "^0.5.1"
+ pkg-dir "^1.0.0"
+
+find-cache-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f"
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^1.0.0"
+ pkg-dir "^2.0.0"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^2.0.0, find-up@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ dependencies:
+ locate-path "^2.0.0"
+
+flat-cache@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481"
+ dependencies:
+ circular-json "^0.3.1"
+ del "^2.0.2"
+ graceful-fs "^4.1.2"
+ write "^0.2.1"
+
+flatten@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
+
+flush-write-stream@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd"
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.4"
+
+follow-redirects@^1.0.0, follow-redirects@^1.2.3:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.0.tgz#234f49cf770b7f35b40e790f636ceba0c3a0ab77"
+ dependencies:
+ debug "^3.1.0"
+
+for-in@^1.0.1, for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ dependencies:
+ for-in "^1.0.1"
+
+foreach@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "1.0.6"
+ mime-types "^2.1.12"
+
+forwarded@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ dependencies:
+ map-cache "^0.2.2"
+
+fresh@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+
+from2@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+
+fs-minipass@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
+ dependencies:
+ minipass "^2.2.1"
+
+fs-write-stream-atomic@^1.0.8:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
+ dependencies:
+ graceful-fs "^4.1.2"
+ iferr "^0.1.5"
+ imurmurhash "^0.1.4"
+ readable-stream "1 || 2"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+fsevents@^1.1.2, fsevents@^1.2.3:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
+ dependencies:
+ nan "^2.9.2"
+ node-pre-gyp "^0.10.0"
+
+function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
+function.prototype.name@^1.0.3:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327"
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ is-callable "^1.1.3"
+
+functional-red-black-tree@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+
+get-stdin@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
+
+get-stream@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ dependencies:
+ is-glob "^2.0.0"
+
+glob-parent@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+ dependencies:
+ is-glob "^3.1.0"
+ path-dirname "^1.0.0"
+
+glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^11.0.1:
+ version "11.5.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.5.0.tgz#6bc840de6771173b191f13d3a9c94d441ee92642"
+
+globals@^9.18.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
+globby@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
+ dependencies:
+ array-union "^1.0.1"
+ arrify "^1.0.0"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+globby@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
+ dependencies:
+ array-union "^1.0.1"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.2:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+
+handle-thing@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
+
+handlebars@^4.0.3:
+ version "4.0.11"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
+ dependencies:
+ async "^1.4.0"
+ optimist "^0.6.1"
+ source-map "^0.4.4"
+ optionalDependencies:
+ uglify-js "^2.6"
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+
+har-validator@~5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
+ dependencies:
+ ajv "^5.1.0"
+ har-schema "^2.0.0"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+
+has-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+has@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+ dependencies:
+ function-bind "^1.0.2"
+
+hash-base@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.0"
+
+he@1.1.x:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
+
+hmac-drbg@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+hosted-git-info@^2.1.4:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222"
+
+hpack.js@^2.1.6:
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
+ dependencies:
+ inherits "^2.0.1"
+ obuf "^1.0.0"
+ readable-stream "^2.0.1"
+ wbuf "^1.1.0"
+
+html-comment-regex@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
+
+html-encoding-sniffer@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+ dependencies:
+ whatwg-encoding "^1.0.1"
+
+html-entities@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
+
+html-minifier@^3.2.3:
+ version "3.5.16"
+ resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.16.tgz#39f5aabaf78bdfc057fe67334226efd7f3851175"
+ dependencies:
+ camel-case "3.0.x"
+ clean-css "4.1.x"
+ commander "2.15.x"
+ he "1.1.x"
+ param-case "2.1.x"
+ relateurl "0.2.x"
+ uglify-js "3.3.x"
+
+html-webpack-plugin@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b"
+ dependencies:
+ html-minifier "^3.2.3"
+ loader-utils "^0.2.16"
+ lodash "^4.17.3"
+ pretty-error "^2.0.2"
+ tapable "^1.0.0"
+ toposort "^1.0.0"
+ util.promisify "1.0.0"
+
+htmlparser2@^3.9.1:
+ version "3.9.2"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
+ dependencies:
+ domelementtype "^1.3.0"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^2.0.2"
+
+htmlparser2@~3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe"
+ dependencies:
+ domelementtype "1"
+ domhandler "2.1"
+ domutils "1.1"
+ readable-stream "1.0"
+
+http-deceiver@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
+
+http-errors@1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
+ dependencies:
+ depd "1.1.1"
+ inherits "2.0.3"
+ setprototypeof "1.0.3"
+ statuses ">= 1.3.1 < 2"
+
+http-errors@~1.6.2:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.3"
+ setprototypeof "1.1.0"
+ statuses ">= 1.4.0 < 2"
+
+http-parser-js@>=0.4.0:
+ version "0.4.13"
+ resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.13.tgz#3bd6d6fde6e3172c9334c3b33b6c193d80fe1137"
+
+http-proxy-middleware@~0.17.4:
+ version "0.17.4"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
+ dependencies:
+ http-proxy "^1.16.2"
+ is-glob "^3.1.0"
+ lodash "^4.17.2"
+ micromatch "^2.3.11"
+
+http-proxy@^1.16.2:
+ version "1.17.0"
+ resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a"
+ dependencies:
+ eventemitter3 "^3.0.0"
+ follow-redirects "^1.0.0"
+ requires-port "^1.0.0"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+https-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
+
+iconv-lite@0.4.19:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+
+iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
+ version "0.4.23"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+icss-replace-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
+
+icss-utils@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962"
+ dependencies:
+ postcss "^6.0.1"
+
+ieee754@^1.1.4:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455"
+
+iferr@^0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
+
+ignore-walk@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
+ dependencies:
+ minimatch "^3.0.4"
+
+ignore@^3.3.3:
+ version "3.3.8"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.8.tgz#3f8e9c35d38708a3a7e0e9abb6c73e7ee7707b2b"
+
+immutable@^3.8.2:
+ version "3.8.2"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
+
+import-local@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc"
+ dependencies:
+ pkg-dir "^2.0.0"
+ resolve-cwd "^2.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+indent-string@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+ dependencies:
+ repeating "^2.0.0"
+
+indexes-of@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+
+indexof@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+
+ini@~1.3.0:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+
+inquirer@^3.0.6:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
+ dependencies:
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.0"
+ cli-cursor "^2.1.0"
+ cli-width "^2.0.0"
+ external-editor "^2.0.4"
+ figures "^2.0.0"
+ lodash "^4.3.0"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rx-lite "^4.0.8"
+ rx-lite-aggregates "^4.0.8"
+ string-width "^2.1.0"
+ strip-ansi "^4.0.0"
+ through "^2.3.6"
+
+internal-ip@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c"
+ dependencies:
+ meow "^3.3.0"
+
+interpret@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
+
+invariant@^2.2.2:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
+ip@^1.1.0, ip@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+
+ipaddr.js@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b"
+
+is-absolute-url@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
+
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ dependencies:
+ kind-of "^6.0.0"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ dependencies:
+ binary-extensions "^1.0.0"
+
+is-boolean-object@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
+
+is-buffer@^1.1.5, is-buffer@~1.1.1:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-callable@^1.1.1, is-callable@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
+
+is-ci@^1.0.10:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5"
+ dependencies:
+ ci-info "^1.0.0"
+
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ dependencies:
+ kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-glob@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+ dependencies:
+ is-extglob "^2.1.0"
+
+is-glob@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number-object@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799"
+
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
+
+is-odd@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24"
+ dependencies:
+ is-number "^4.0.0"
+
+is-path-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
+
+is-path-in-cwd@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52"
+ dependencies:
+ is-path-inside "^1.0.0"
+
+is-path-inside@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-plain-obj@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+
+is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ dependencies:
+ isobject "^3.0.1"
+
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+
+is-promise@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+
+is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ dependencies:
+ has "^1.0.1"
+
+is-resolvable@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+
+is-stream@^1.0.1, is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+is-string@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64"
+
+is-subset@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+
+is-svg@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9"
+ dependencies:
+ html-comment-regex "^1.1.0"
+
+is-symbol@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+is-utf8@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+
+is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+
+is-wsl@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+
+isarray@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+
+isomorphic-fetch@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+ dependencies:
+ node-fetch "^1.0.1"
+ whatwg-fetch ">=0.10.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+istanbul-api@^1.1.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.3.1.tgz#4c3b05d18c0016d1022e079b98dc82c40f488954"
+ dependencies:
+ async "^2.1.4"
+ compare-versions "^3.1.0"
+ fileset "^2.0.2"
+ istanbul-lib-coverage "^1.2.0"
+ istanbul-lib-hook "^1.2.0"
+ istanbul-lib-instrument "^1.10.1"
+ istanbul-lib-report "^1.1.4"
+ istanbul-lib-source-maps "^1.2.4"
+ istanbul-reports "^1.3.0"
+ js-yaml "^3.7.0"
+ mkdirp "^0.5.1"
+ once "^1.4.0"
+
+istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.1.2, istanbul-lib-coverage@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341"
+
+istanbul-lib-hook@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.0.tgz#ae556fd5a41a6e8efa0b1002b1e416dfeaf9816c"
+ dependencies:
+ append-transform "^0.4.0"
+
+istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.4.2:
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz#724b4b6caceba8692d3f1f9d0727e279c401af7b"
+ dependencies:
+ babel-generator "^6.18.0"
+ babel-template "^6.16.0"
+ babel-traverse "^6.18.0"
+ babel-types "^6.18.0"
+ babylon "^6.18.0"
+ istanbul-lib-coverage "^1.2.0"
+ semver "^5.3.0"
+
+istanbul-lib-report@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz#e886cdf505c4ebbd8e099e4396a90d0a28e2acb5"
+ dependencies:
+ istanbul-lib-coverage "^1.2.0"
+ mkdirp "^0.5.1"
+ path-parse "^1.0.5"
+ supports-color "^3.1.2"
+
+istanbul-lib-source-maps@^1.1.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.3.tgz#20fb54b14e14b3fb6edb6aca3571fd2143db44e6"
+ dependencies:
+ debug "^3.1.0"
+ istanbul-lib-coverage "^1.1.2"
+ mkdirp "^0.5.1"
+ rimraf "^2.6.1"
+ source-map "^0.5.3"
+
+istanbul-lib-source-maps@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.4.tgz#cc7ccad61629f4efff8e2f78adb8c522c9976ec7"
+ dependencies:
+ debug "^3.1.0"
+ istanbul-lib-coverage "^1.2.0"
+ mkdirp "^0.5.1"
+ rimraf "^2.6.1"
+ source-map "^0.5.3"
+
+istanbul-reports@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.3.0.tgz#2f322e81e1d9520767597dca3c20a0cce89a3554"
+ dependencies:
+ handlebars "^4.0.3"
+
+jest-changed-files@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-21.2.0.tgz#5dbeecad42f5d88b482334902ce1cba6d9798d29"
+ dependencies:
+ throat "^4.0.0"
+
+jest-cli@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-21.2.1.tgz#9c528b6629d651911138d228bdb033c157ec8c00"
+ dependencies:
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ glob "^7.1.2"
+ graceful-fs "^4.1.11"
+ is-ci "^1.0.10"
+ istanbul-api "^1.1.1"
+ istanbul-lib-coverage "^1.0.1"
+ istanbul-lib-instrument "^1.4.2"
+ istanbul-lib-source-maps "^1.1.0"
+ jest-changed-files "^21.2.0"
+ jest-config "^21.2.1"
+ jest-environment-jsdom "^21.2.1"
+ jest-haste-map "^21.2.0"
+ jest-message-util "^21.2.1"
+ jest-regex-util "^21.2.0"
+ jest-resolve-dependencies "^21.2.0"
+ jest-runner "^21.2.1"
+ jest-runtime "^21.2.1"
+ jest-snapshot "^21.2.1"
+ jest-util "^21.2.1"
+ micromatch "^2.3.11"
+ node-notifier "^5.0.2"
+ pify "^3.0.0"
+ slash "^1.0.0"
+ string-length "^2.0.0"
+ strip-ansi "^4.0.0"
+ which "^1.2.12"
+ worker-farm "^1.3.1"
+ yargs "^9.0.0"
+
+jest-config@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-21.2.1.tgz#c7586c79ead0bcc1f38c401e55f964f13bf2a480"
+ dependencies:
+ chalk "^2.0.1"
+ glob "^7.1.1"
+ jest-environment-jsdom "^21.2.1"
+ jest-environment-node "^21.2.1"
+ jest-get-type "^21.2.0"
+ jest-jasmine2 "^21.2.1"
+ jest-regex-util "^21.2.0"
+ jest-resolve "^21.2.0"
+ jest-util "^21.2.1"
+ jest-validate "^21.2.1"
+ pretty-format "^21.2.1"
+
+jest-diff@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-21.2.1.tgz#46cccb6cab2d02ce98bc314011764bb95b065b4f"
+ dependencies:
+ chalk "^2.0.1"
+ diff "^3.2.0"
+ jest-get-type "^21.2.0"
+ pretty-format "^21.2.1"
+
+jest-docblock@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414"
+
+jest-environment-jsdom@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-21.2.1.tgz#38d9980c8259b2a608ec232deee6289a60d9d5b4"
+ dependencies:
+ jest-mock "^21.2.0"
+ jest-util "^21.2.1"
+ jsdom "^9.12.0"
+
+jest-environment-node@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-21.2.1.tgz#98c67df5663c7fbe20f6e792ac2272c740d3b8c8"
+ dependencies:
+ jest-mock "^21.2.0"
+ jest-util "^21.2.1"
+
+jest-get-type@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.2.0.tgz#f6376ab9db4b60d81e39f30749c6c466f40d4a23"
+
+jest-haste-map@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-21.2.0.tgz#1363f0a8bb4338f24f001806571eff7a4b2ff3d8"
+ dependencies:
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.1.11"
+ jest-docblock "^21.2.0"
+ micromatch "^2.3.11"
+ sane "^2.0.0"
+ worker-farm "^1.3.1"
+
+jest-jasmine2@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-21.2.1.tgz#9cc6fc108accfa97efebce10c4308548a4ea7592"
+ dependencies:
+ chalk "^2.0.1"
+ expect "^21.2.1"
+ graceful-fs "^4.1.11"
+ jest-diff "^21.2.1"
+ jest-matcher-utils "^21.2.1"
+ jest-message-util "^21.2.1"
+ jest-snapshot "^21.2.1"
+ p-cancelable "^0.3.0"
+
+jest-matcher-utils@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-21.2.1.tgz#72c826eaba41a093ac2b4565f865eb8475de0f64"
+ dependencies:
+ chalk "^2.0.1"
+ jest-get-type "^21.2.0"
+ pretty-format "^21.2.1"
+
+jest-message-util@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-21.2.1.tgz#bfe5d4692c84c827d1dcf41823795558f0a1acbe"
+ dependencies:
+ chalk "^2.0.1"
+ micromatch "^2.3.11"
+ slash "^1.0.0"
+
+jest-mock@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-21.2.0.tgz#7eb0770e7317968165f61ea2a7281131534b3c0f"
+
+jest-regex-util@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-21.2.0.tgz#1b1e33e63143babc3e0f2e6c9b5ba1eb34b2d530"
+
+jest-resolve-dependencies@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-21.2.0.tgz#9e231e371e1a736a1ad4e4b9a843bc72bfe03d09"
+ dependencies:
+ jest-regex-util "^21.2.0"
+
+jest-resolve@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-21.2.0.tgz#068913ad2ba6a20218e5fd32471f3874005de3a6"
+ dependencies:
+ browser-resolve "^1.11.2"
+ chalk "^2.0.1"
+ is-builtin-module "^1.0.0"
+
+jest-runner@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-21.2.1.tgz#194732e3e518bfb3d7cbfc0fd5871246c7e1a467"
+ dependencies:
+ jest-config "^21.2.1"
+ jest-docblock "^21.2.0"
+ jest-haste-map "^21.2.0"
+ jest-jasmine2 "^21.2.1"
+ jest-message-util "^21.2.1"
+ jest-runtime "^21.2.1"
+ jest-util "^21.2.1"
+ pify "^3.0.0"
+ throat "^4.0.0"
+ worker-farm "^1.3.1"
+
+jest-runtime@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-21.2.1.tgz#99dce15309c670442eee2ebe1ff53a3cbdbbb73e"
+ dependencies:
+ babel-core "^6.0.0"
+ babel-jest "^21.2.0"
+ babel-plugin-istanbul "^4.0.0"
+ chalk "^2.0.1"
+ convert-source-map "^1.4.0"
+ graceful-fs "^4.1.11"
+ jest-config "^21.2.1"
+ jest-haste-map "^21.2.0"
+ jest-regex-util "^21.2.0"
+ jest-resolve "^21.2.0"
+ jest-util "^21.2.1"
+ json-stable-stringify "^1.0.1"
+ micromatch "^2.3.11"
+ slash "^1.0.0"
+ strip-bom "3.0.0"
+ write-file-atomic "^2.1.0"
+ yargs "^9.0.0"
+
+jest-snapshot@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-21.2.1.tgz#29e49f16202416e47343e757e5eff948c07fd7b0"
+ dependencies:
+ chalk "^2.0.1"
+ jest-diff "^21.2.1"
+ jest-matcher-utils "^21.2.1"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ pretty-format "^21.2.1"
+
+jest-util@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-21.2.1.tgz#a274b2f726b0897494d694a6c3d6a61ab819bb78"
+ dependencies:
+ callsites "^2.0.0"
+ chalk "^2.0.1"
+ graceful-fs "^4.1.11"
+ jest-message-util "^21.2.1"
+ jest-mock "^21.2.0"
+ jest-validate "^21.2.1"
+ mkdirp "^0.5.1"
+
+jest-validate@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.2.1.tgz#cc0cbca653cd54937ba4f2a111796774530dd3c7"
+ dependencies:
+ chalk "^2.0.1"
+ jest-get-type "^21.2.0"
+ leven "^2.1.0"
+ pretty-format "^21.2.1"
+
+jest@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-21.2.1.tgz#c964e0b47383768a1438e3ccf3c3d470327604e1"
+ dependencies:
+ jest-cli "^21.2.1"
+
+js-base64@^2.1.9:
+ version "2.4.5"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.5.tgz#e293cd3c7c82f070d700fc7a1ca0a2e69f101f92"
+
+js-tokens@^3.0.0, js-tokens@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@^3.7.0, js-yaml@^3.9.1:
+ version "3.11.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+js-yaml@~3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^2.6.0"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
+jsdom@^9.12.0:
+ version "9.12.0"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4"
+ dependencies:
+ abab "^1.0.3"
+ acorn "^4.0.4"
+ acorn-globals "^3.1.0"
+ array-equal "^1.0.0"
+ content-type-parser "^1.0.1"
+ cssom ">= 0.3.2 < 0.4.0"
+ cssstyle ">= 0.2.37 < 0.3.0"
+ escodegen "^1.6.1"
+ html-encoding-sniffer "^1.0.1"
+ nwmatcher ">= 1.3.9 < 2.0.0"
+ parse5 "^1.5.1"
+ request "^2.79.0"
+ sax "^1.2.1"
+ symbol-tree "^3.2.1"
+ tough-cookie "^2.3.2"
+ webidl-conversions "^4.0.0"
+ whatwg-encoding "^1.0.1"
+ whatwg-url "^4.3.0"
+ xml-name-validator "^2.0.1"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+
+json-loader@^0.5.4:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
+
+json-schema-traverse@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stable-stringify-without-jsonify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+
+json-stable-stringify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+json3@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
+
+json5@^0.5.0, json5@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+jsx-ast-utils@^2.0.0, jsx-ast-utils@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
+ dependencies:
+ array-includes "^3.0.3"
+
+killable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+
+lazy-cache@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ dependencies:
+ invert-kv "^1.0.0"
+
+leven@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
+
+levn@^0.3.0, levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+load-json-file@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+
+load-json-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
+loader-runner@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
+
+loader-utils@^0.2.16:
+ version "0.2.17"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
+ dependencies:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+ object-assign "^4.0.1"
+
+loader-utils@^1.0.2, loader-utils@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
+ dependencies:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+lodash.camelcase@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
+
+lodash.flattendeep@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+
+lodash.memoize@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+
+lodash.throttle@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
+
+lodash.uniq@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+
+lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0:
+ version "4.17.10"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
+
+loglevel@^1.4.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
+
+longest@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ dependencies:
+ js-tokens "^3.0.0"
+
+loud-rejection@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+ dependencies:
+ currently-unhandled "^0.4.1"
+ signal-exit "^3.0.0"
+
+lower-case@^1.1.1:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
+
+lru-cache@^4.0.1, lru-cache@^4.1.1:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+make-dir@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
+ dependencies:
+ pify "^3.0.0"
+
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ dependencies:
+ tmpl "1.0.x"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+
+map-obj@^1.0.0, map-obj@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ dependencies:
+ object-visit "^1.0.0"
+
+math-expression-evaluator@^1.2.14:
+ version "1.2.17"
+ resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
+
+math-random@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac"
+
+md5.js@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d"
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+md5@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9"
+ dependencies:
+ charenc "~0.0.1"
+ crypt "~0.0.1"
+ is-buffer "~1.1.1"
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+
+mem@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+memory-fs@^0.4.0, memory-fs@~0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+ dependencies:
+ errno "^0.1.3"
+ readable-stream "^2.0.1"
+
+meow@^3.3.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+ dependencies:
+ camelcase-keys "^2.0.0"
+ decamelize "^1.1.2"
+ loud-rejection "^1.0.0"
+ map-obj "^1.0.1"
+ minimist "^1.1.3"
+ normalize-package-data "^2.3.4"
+ object-assign "^4.0.1"
+ read-pkg-up "^1.0.1"
+ redent "^1.0.0"
+ trim-newlines "^1.0.0"
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+
+merge@^1.1.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+
+micromatch@^2.3.11:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+micromatch@^3.1.4, micromatch@^3.1.8:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
+miller-rabin@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+"mime-db@>= 1.33.0 < 2", mime-db@~1.33.0:
+ version "1.33.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
+
+mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18:
+ version "2.1.18"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
+ dependencies:
+ mime-db "~1.33.0"
+
+mime@1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
+
+mime@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+
+mimic-fn@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
+
+minimalistic-assert@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+
+minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+minimist@~0.0.1:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+
+minipass@^2.2.1, minipass@^2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233"
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
+minizlib@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
+ dependencies:
+ minipass "^2.2.1"
+
+mississippi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
+ dependencies:
+ concat-stream "^1.5.0"
+ duplexify "^3.4.2"
+ end-of-stream "^1.1.0"
+ flush-write-stream "^1.0.0"
+ from2 "^2.1.0"
+ parallel-transform "^1.1.0"
+ pump "^2.0.1"
+ pumpify "^1.3.3"
+ stream-each "^1.1.0"
+ through2 "^2.0.0"
+
+mixin-deep@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+move-concurrently@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
+ dependencies:
+ aproba "^1.1.1"
+ copy-concurrently "^1.0.0"
+ fs-write-stream-atomic "^1.0.8"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.3"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+multicast-dns-service-types@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
+
+multicast-dns@^6.0.1:
+ version "6.2.3"
+ resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229"
+ dependencies:
+ dns-packet "^1.3.1"
+ thunky "^1.0.2"
+
+mute-stream@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+
+nan@^2.9.2:
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
+
+nanomatch@^1.2.9:
+ version "1.2.9"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-odd "^2.0.0"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+nearley@^2.7.10:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.13.0.tgz#6e7b0f4e68bfc3e74c99eaef2eda39e513143439"
+ dependencies:
+ nomnom "~1.6.2"
+ railroad-diagrams "^1.0.0"
+ randexp "0.4.6"
+ semver "^5.4.1"
+
+needle@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d"
+ dependencies:
+ debug "^2.1.2"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
+negotiator@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+
+neo-async@^2.5.0:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.1.tgz#acb909e327b1e87ec9ef15f41b8a269512ad41ee"
+
+no-case@^2.2.0:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
+ dependencies:
+ lower-case "^1.1.1"
+
+node-fetch@^1.0.1:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+ dependencies:
+ encoding "^0.1.11"
+ is-stream "^1.0.1"
+
+node-forge@0.7.5:
+ version "0.7.5"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df"
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+
+node-libs-browser@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.2.0"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^1.0.0"
+ https-browserify "^1.0.0"
+ os-browserify "^0.3.0"
+ path-browserify "0.0.0"
+ process "^0.11.10"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.3.3"
+ stream-browserify "^2.0.1"
+ stream-http "^2.7.2"
+ string_decoder "^1.0.0"
+ timers-browserify "^2.0.4"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.10.3"
+ vm-browserify "0.0.4"
+
+node-notifier@^5.0.2:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea"
+ dependencies:
+ growly "^1.3.0"
+ semver "^5.4.1"
+ shellwords "^0.1.1"
+ which "^1.3.0"
+
+node-pre-gyp@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz#6e4ef5bb5c5203c6552448828c852c40111aac46"
+ dependencies:
+ detect-libc "^1.0.2"
+ mkdirp "^0.5.1"
+ needle "^2.2.0"
+ nopt "^4.0.1"
+ npm-packlist "^1.1.6"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^4"
+
+nomnom@~1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.6.2.tgz#84a66a260174408fc5b77a18f888eccc44fb6971"
+ dependencies:
+ colors "0.5.x"
+ underscore "~1.4.4"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.0.1, normalize-path@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+normalize-range@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+
+normalize-url@^1.4.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
+ dependencies:
+ object-assign "^4.0.1"
+ prepend-http "^1.0.0"
+ query-string "^4.1.0"
+ sort-keys "^1.0.0"
+
+npm-bundled@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308"
+
+npm-packlist@^1.1.6:
+ version "1.1.10"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a"
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ dependencies:
+ path-key "^2.0.0"
+
+npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+nth-check@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4"
+ dependencies:
+ boolbase "~1.0.0"
+
+num2fraction@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+"nwmatcher@>= 1.3.9 < 2.0.0":
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e"
+
+oauth-sign@~0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+
+object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
+object-inspect@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
+
+object-is@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
+
+object-keys@^1.0.11, object-keys@^1.0.8:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ dependencies:
+ isobject "^3.0.0"
+
+object.assign@^4.0.4, object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+object.entries@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.0.4.tgz#1bf9a4dd2288f5b33f3a993d257661f05d161a5f"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.6.1"
+ function-bind "^1.1.0"
+ has "^1.0.1"
+
+object.getownpropertydescriptors@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.1"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ dependencies:
+ isobject "^3.0.1"
+
+object.values@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.6.1"
+ function-bind "^1.1.0"
+ has "^1.0.1"
+
+obuf@^1.0.0, obuf@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ dependencies:
+ ee-first "1.1.1"
+
+on-headers@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+onetime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+opn@^5.1.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c"
+ dependencies:
+ is-wsl "^1.1.0"
+
+optimist@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+ dependencies:
+ minimist "~0.0.1"
+ wordwrap "~0.0.2"
+
+optionator@^0.8.1, optionator@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+original@>=0.0.5:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/original/-/original-1.0.1.tgz#b0a53ff42ba997a8c9cd1fb5daaeb42b9d693190"
+ dependencies:
+ url-parse "~1.4.0"
+
+os-browserify@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-locale@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+ dependencies:
+ lcid "^1.0.0"
+
+os-locale@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
+ dependencies:
+ execa "^0.7.0"
+ lcid "^1.0.0"
+ mem "^1.1.0"
+
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+osenv@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+p-cancelable@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
+p-limit@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
+ dependencies:
+ p-try "^1.0.0"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ dependencies:
+ p-limit "^1.1.0"
+
+p-map@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
+
+p-try@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+
+pako@~1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
+
+parallel-transform@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"
+ dependencies:
+ cyclist "~0.2.2"
+ inherits "^2.0.3"
+ readable-stream "^2.1.5"
+
+param-case@2.1.x:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"
+ dependencies:
+ no-case "^2.2.0"
+
+parse-asn1@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8"
+ dependencies:
+ asn1.js "^4.0.0"
+ browserify-aes "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.0"
+ pbkdf2 "^3.0.3"
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ dependencies:
+ error-ex "^1.2.0"
+
+parse5@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
+
+parse5@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+ dependencies:
+ "@types/node" "*"
+
+parseurl@~1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+
+path-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
+
+path-dirname@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ dependencies:
+ pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-is-inside@^1.0.1, path-is-inside@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+
+path-key@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
+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"
+
+path-type@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+ dependencies:
+ pify "^2.0.0"
+
+pbkdf2@^3.0.3:
+ version "3.0.16"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c"
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+
+pify@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ dependencies:
+ pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+
+pkg-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+ dependencies:
+ find-up "^1.0.0"
+
+pkg-dir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
+ dependencies:
+ find-up "^2.1.0"
+
+pluralize@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
+
+portfinder@^1.0.9:
+ version "1.0.13"
+ resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
+ dependencies:
+ async "^1.5.2"
+ debug "^2.2.0"
+ mkdirp "0.5.x"
+
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+
+postcss-calc@^5.2.0:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e"
+ dependencies:
+ postcss "^5.0.2"
+ postcss-message-helpers "^2.0.0"
+ reduce-css-calc "^1.2.6"
+
+postcss-colormin@^2.1.8:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b"
+ dependencies:
+ colormin "^1.0.5"
+ postcss "^5.0.13"
+ postcss-value-parser "^3.2.3"
+
+postcss-convert-values@^2.3.4:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d"
+ dependencies:
+ postcss "^5.0.11"
+ postcss-value-parser "^3.1.2"
+
+postcss-discard-comments@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d"
+ dependencies:
+ postcss "^5.0.14"
+
+postcss-discard-duplicates@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932"
+ dependencies:
+ postcss "^5.0.4"
+
+postcss-discard-empty@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5"
+ dependencies:
+ postcss "^5.0.14"
+
+postcss-discard-overridden@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58"
+ dependencies:
+ postcss "^5.0.16"
+
+postcss-discard-unused@^2.2.1:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433"
+ dependencies:
+ postcss "^5.0.14"
+ uniqs "^2.0.0"
+
+postcss-filter-plugins@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.3.tgz#82245fdf82337041645e477114d8e593aa18b8ec"
+ dependencies:
+ postcss "^5.0.4"
+
+postcss-merge-idents@^2.1.5:
+ version "2.1.7"
+ resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270"
+ dependencies:
+ has "^1.0.1"
+ postcss "^5.0.10"
+ postcss-value-parser "^3.1.1"
+
+postcss-merge-longhand@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658"
+ dependencies:
+ postcss "^5.0.4"
+
+postcss-merge-rules@^2.0.3:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721"
+ dependencies:
+ browserslist "^1.5.2"
+ caniuse-api "^1.5.2"
+ postcss "^5.0.4"
+ postcss-selector-parser "^2.2.2"
+ vendors "^1.0.0"
+
+postcss-message-helpers@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e"
+
+postcss-minify-font-values@^1.0.2:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69"
+ dependencies:
+ object-assign "^4.0.1"
+ postcss "^5.0.4"
+ postcss-value-parser "^3.0.2"
+
+postcss-minify-gradients@^1.0.1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1"
+ dependencies:
+ postcss "^5.0.12"
+ postcss-value-parser "^3.3.0"
+
+postcss-minify-params@^1.0.4:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3"
+ dependencies:
+ alphanum-sort "^1.0.1"
+ postcss "^5.0.2"
+ postcss-value-parser "^3.0.2"
+ uniqs "^2.0.0"
+
+postcss-minify-selectors@^2.0.4:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf"
+ dependencies:
+ alphanum-sort "^1.0.2"
+ has "^1.0.1"
+ postcss "^5.0.14"
+ postcss-selector-parser "^2.0.0"
+
+postcss-modules-extract-imports@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85"
+ dependencies:
+ postcss "^6.0.1"
+
+postcss-modules-local-by-default@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069"
+ dependencies:
+ css-selector-tokenizer "^0.7.0"
+ postcss "^6.0.1"
+
+postcss-modules-scope@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
+ dependencies:
+ css-selector-tokenizer "^0.7.0"
+ postcss "^6.0.1"
+
+postcss-modules-values@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
+ dependencies:
+ icss-replace-symbols "^1.1.0"
+ postcss "^6.0.1"
+
+postcss-normalize-charset@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1"
+ dependencies:
+ postcss "^5.0.5"
+
+postcss-normalize-url@^3.0.7:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222"
+ dependencies:
+ is-absolute-url "^2.0.0"
+ normalize-url "^1.4.0"
+ postcss "^5.0.14"
+ postcss-value-parser "^3.2.3"
+
+postcss-ordered-values@^2.1.0:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d"
+ dependencies:
+ postcss "^5.0.4"
+ postcss-value-parser "^3.0.1"
+
+postcss-reduce-idents@^2.2.2:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3"
+ dependencies:
+ postcss "^5.0.4"
+ postcss-value-parser "^3.0.2"
+
+postcss-reduce-initial@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea"
+ dependencies:
+ postcss "^5.0.4"
+
+postcss-reduce-transforms@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1"
+ dependencies:
+ has "^1.0.1"
+ postcss "^5.0.8"
+ postcss-value-parser "^3.0.1"
+
+postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90"
+ dependencies:
+ flatten "^1.0.2"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss-svgo@^2.1.1:
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d"
+ dependencies:
+ is-svg "^2.0.0"
+ postcss "^5.0.14"
+ postcss-value-parser "^3.2.3"
+ svgo "^0.7.0"
+
+postcss-unique-selectors@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d"
+ dependencies:
+ alphanum-sort "^1.0.1"
+ postcss "^5.0.4"
+ uniqs "^2.0.0"
+
+postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
+
+postcss-zindex@^2.0.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22"
+ dependencies:
+ has "^1.0.1"
+ postcss "^5.0.4"
+ uniqs "^2.0.0"
+
+postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16:
+ version "5.2.18"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5"
+ dependencies:
+ chalk "^1.1.3"
+ js-base64 "^2.1.9"
+ source-map "^0.5.6"
+ supports-color "^3.2.3"
+
+postcss@^6.0.1:
+ version "6.0.22"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3"
+ dependencies:
+ chalk "^2.4.1"
+ source-map "^0.6.1"
+ supports-color "^5.4.0"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+prepend-http@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+
+pretty-error@^2.0.2:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
+ dependencies:
+ renderkid "^2.0.1"
+ utila "~0.4"
+
+pretty-format@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-21.2.1.tgz#ae5407f3cf21066cd011aa1ba5fce7b6a2eddb36"
+ dependencies:
+ ansi-regex "^3.0.0"
+ ansi-styles "^3.2.0"
+
+private@^0.1.6, private@^0.1.8:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+
+process-nextick-args@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
+
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+
+progress@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
+
+promise-inflight@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+
+promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ dependencies:
+ asap "~2.0.3"
+
+prop-types@^15.5.10, prop-types@^15.6.0:
+ version "15.6.1"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.3.1"
+ object-assign "^4.1.1"
+
+proxy-addr@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341"
+ dependencies:
+ forwarded "~0.1.2"
+ ipaddr.js "1.6.0"
+
+prr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
+public-encrypt@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.2.tgz#46eb9107206bf73489f8b85b69d91334c6610994"
+ dependencies:
+ bn.js "^4.1.0"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ parse-asn1 "^5.0.0"
+ randombytes "^2.0.1"
+
+pump@^2.0.0, pump@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+pumpify@^1.3.3:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
+ dependencies:
+ duplexify "^3.6.0"
+ inherits "^2.0.3"
+ pump "^2.0.0"
+
+punycode@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+
+punycode@^1.2.4, punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+punycode@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+
+q@^1.1.2:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+
+qs@6.5.1:
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+
+qs@~6.5.1:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+
+query-string@^4.1.0:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
+ dependencies:
+ object-assign "^4.1.0"
+ strict-uri-encode "^1.0.0"
+
+querystring-es3@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+
+querystring@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+
+querystringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755"
+
+raf@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
+ dependencies:
+ performance-now "^2.1.0"
+
+railroad-diagrams@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+
+randexp@0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+ dependencies:
+ discontinuous-range "1.0.0"
+ ret "~0.1.10"
+
+randomatic@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.0.0.tgz#d35490030eb4f7578de292ce6dfb04a91a128923"
+ dependencies:
+ is-number "^4.0.0"
+ kind-of "^6.0.0"
+ math-random "^1.0.1"
+
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
+ dependencies:
+ safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458"
+ dependencies:
+ randombytes "^2.0.5"
+ safe-buffer "^5.1.0"
+
+range-parser@^1.0.3, range-parser@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+
+raw-body@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
+ dependencies:
+ bytes "3.0.0"
+ http-errors "1.6.2"
+ iconv-lite "0.4.19"
+ unpipe "1.0.0"
+
+rc@^1.1.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.7.tgz#8a10ca30d588d00464360372b890d06dacd02297"
+ dependencies:
+ deep-extend "^0.5.1"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+react-addons-test-utils@^15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156"
+
+react-dom@^15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730"
+ dependencies:
+ fbjs "^0.8.9"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.0"
+ prop-types "^15.5.10"
+
+react-icon-base@2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.0.tgz#a196e33fdf1e7aaa1fda3aefbb68bdad9e82a79d"
+
+react-icons@^2.2.7:
+ version "2.2.7"
+ resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-2.2.7.tgz#d7860826b258557510dac10680abea5ca23cf650"
+ dependencies:
+ react-icon-base "2.1.0"
+
+react-test-renderer@15:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.6.2.tgz#d0333434fc2c438092696ca770da5ed48037efa8"
+ dependencies:
+ fbjs "^0.8.9"
+ object-assign "^4.1.0"
+
+react@^15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
+ dependencies:
+ create-react-class "^15.6.0"
+ fbjs "^0.8.9"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.0"
+ prop-types "^15.5.10"
+
+read-pkg-up@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+ dependencies:
+ find-up "^1.0.0"
+ read-pkg "^1.0.0"
+
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+ dependencies:
+ find-up "^2.0.0"
+ read-pkg "^2.0.0"
+
+read-pkg@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+ dependencies:
+ load-json-file "^1.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^1.0.0"
+
+read-pkg@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+ dependencies:
+ load-json-file "^2.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^2.0.0"
+
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.6:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+readable-stream@1.0:
+ version "1.0.34"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
+readdirp@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
+ dependencies:
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ readable-stream "^2.0.2"
+ set-immediate-shim "^1.0.1"
+
+redent@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+ dependencies:
+ indent-string "^2.1.0"
+ strip-indent "^1.0.1"
+
+reduce-css-calc@^1.2.6:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
+ dependencies:
+ balanced-match "^0.4.2"
+ math-expression-evaluator "^1.2.14"
+ reduce-function-call "^1.0.1"
+
+reduce-function-call@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99"
+ dependencies:
+ balanced-match "^0.4.2"
+
+regenerate@^1.2.1:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
+
+regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+
+regenerator-transform@^0.10.0:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
+ dependencies:
+ babel-runtime "^6.18.0"
+ babel-types "^6.19.0"
+ private "^0.1.6"
+
+regex-cache@^0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
+ dependencies:
+ is-equal-shallow "^0.1.3"
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
+regexpp@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab"
+
+regexpu-core@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regexpu-core@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regjsgen@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
+
+regjsparser@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
+ dependencies:
+ jsesc "~0.5.0"
+
+relateurl@0.2.x:
+ version "0.2.7"
+ resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
+
+remove-trailing-separator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+
+renderkid@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.1.tgz#898cabfc8bede4b7b91135a3ffd323e58c0db319"
+ dependencies:
+ css-select "^1.1.0"
+ dom-converter "~0.1"
+ htmlparser2 "~3.3.0"
+ strip-ansi "^3.0.0"
+ utila "~0.3"
+
+repeat-element@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+
+repeat-string@^1.5.2, repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ dependencies:
+ is-finite "^1.0.0"
+
+request@^2.79.0:
+ version "2.87.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.6.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.1"
+ forever-agent "~0.6.1"
+ form-data "~2.3.1"
+ har-validator "~5.0.3"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.17"
+ oauth-sign "~0.8.2"
+ performance-now "^2.1.0"
+ qs "~6.5.1"
+ safe-buffer "^5.1.1"
+ tough-cookie "~2.3.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.1.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+require-uncached@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
+ dependencies:
+ caller-path "^0.1.0"
+ resolve-from "^1.0.0"
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+
+resolve-cwd@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
+ dependencies:
+ resolve-from "^3.0.0"
+
+resolve-from@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
+
+resolve-from@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+
+resolve@1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+
+resolve@^1.5.0, resolve@^1.6.0:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
+ dependencies:
+ path-parse "^1.0.5"
+
+restore-cursor@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+ dependencies:
+ onetime "^2.0.0"
+ signal-exit "^3.0.2"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+
+right-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
+ dependencies:
+ align-text "^0.1.1"
+
+rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
+ dependencies:
+ glob "^7.0.5"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+rst-selector-parser@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
+ dependencies:
+ lodash.flattendeep "^4.4.0"
+ nearley "^2.7.10"
+
+rsvp@^3.3.3:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
+
+run-async@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
+ dependencies:
+ is-promise "^2.1.0"
+
+run-queue@^1.0.0, run-queue@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
+ dependencies:
+ aproba "^1.1.1"
+
+rx-lite-aggregates@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
+ dependencies:
+ rx-lite "*"
+
+rx-lite@*, rx-lite@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
+
+safe-buffer@5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ dependencies:
+ ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+
+sane@^2.0.0:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa"
+ dependencies:
+ anymatch "^2.0.0"
+ capture-exit "^1.2.0"
+ exec-sh "^0.2.0"
+ fb-watchman "^2.0.0"
+ micromatch "^3.1.4"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+ watch "~0.18.0"
+ optionalDependencies:
+ fsevents "^1.2.3"
+
+sax@^1.2.1, sax@^1.2.4, sax@~1.2.1:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+
+schema-utils@^0.4.5:
+ version "0.4.5"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e"
+ dependencies:
+ ajv "^6.1.0"
+ ajv-keywords "^3.1.0"
+
+select-hose@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
+
+selfsigned@^1.9.1:
+ version "1.10.3"
+ resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.3.tgz#d628ecf9e3735f84e8bafba936b3cf85bea43823"
+ dependencies:
+ node-forge "0.7.5"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
+
+send@0.16.2:
+ version "0.16.2"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
+ dependencies:
+ debug "2.6.9"
+ depd "~1.1.2"
+ destroy "~1.0.4"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "0.5.2"
+ http-errors "~1.6.2"
+ mime "1.4.1"
+ ms "2.0.0"
+ on-finished "~2.3.0"
+ range-parser "~1.2.0"
+ statuses "~1.4.0"
+
+serialize-javascript@^1.4.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe"
+
+serve-index@^1.7.2:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"
+ dependencies:
+ accepts "~1.3.4"
+ batch "0.6.1"
+ debug "2.6.9"
+ escape-html "~1.0.3"
+ http-errors "~1.6.2"
+ mime-types "~2.1.17"
+ parseurl "~1.3.2"
+
+serve-static@1.13.2:
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1"
+ dependencies:
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ parseurl "~1.3.2"
+ send "0.16.2"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+set-immediate-shim@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+
+set-value@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.1"
+ to-object-path "^0.3.0"
+
+set-value@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
+setimmediate@^1.0.4, setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+
+setprototypeof@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
+
+setprototypeof@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+ version "2.4.11"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+shellwords@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+slice-ansi@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^3.1.0"
+
+sockjs-client@1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12"
+ dependencies:
+ debug "^2.6.6"
+ eventsource "0.1.6"
+ faye-websocket "~0.11.0"
+ inherits "^2.0.1"
+ json3 "^3.3.2"
+ url-parse "^1.1.8"
+
+sockjs@0.3.19:
+ version "0.3.19"
+ resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d"
+ dependencies:
+ faye-websocket "^0.10.0"
+ uuid "^3.0.1"
+
+sort-keys@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
+ dependencies:
+ is-plain-obj "^1.0.0"
+
+source-list-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
+
+source-map-resolve@^0.5.0:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
+ dependencies:
+ atob "^2.1.1"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
+source-map-support@^0.4.15:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
+ dependencies:
+ source-map "^0.5.6"
+
+source-map-url@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+
+source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
+source-map@^0.4.4:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
+ dependencies:
+ amdefine ">=0.0.4"
+
+source-map@^0.6.1, source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
+spdx-correct@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82"
+ dependencies:
+ spdx-expression-parse "^3.0.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9"
+
+spdx-expression-parse@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
+ dependencies:
+ spdx-exceptions "^2.1.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87"
+
+spdy-transport@^2.0.18:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.1.0.tgz#4bbb15aaffed0beefdd56ad61dbdc8ba3e2cb7a1"
+ dependencies:
+ debug "^2.6.8"
+ detect-node "^2.0.3"
+ hpack.js "^2.1.6"
+ obuf "^1.1.1"
+ readable-stream "^2.2.9"
+ safe-buffer "^5.0.1"
+ wbuf "^1.7.2"
+
+spdy@^3.4.1:
+ version "3.4.7"
+ resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc"
+ dependencies:
+ debug "^2.6.8"
+ handle-thing "^1.2.5"
+ http-deceiver "^1.2.7"
+ safe-buffer "^5.0.1"
+ select-hose "^2.0.0"
+ spdy-transport "^2.0.18"
+
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ dependencies:
+ extend-shallow "^3.0.0"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
+sshpk@^1.7.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ dashdash "^1.12.0"
+ getpass "^0.1.1"
+ optionalDependencies:
+ bcrypt-pbkdf "^1.0.0"
+ ecc-jsbn "~0.1.1"
+ jsbn "~0.1.0"
+ tweetnacl "~0.14.0"
+
+ssri@^5.2.4:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06"
+ dependencies:
+ safe-buffer "^5.1.1"
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
+"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+
+statuses@~1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
+
+stream-browserify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-each@^1.1.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd"
+ dependencies:
+ end-of-stream "^1.1.0"
+ stream-shift "^1.0.0"
+
+stream-http@^2.7.2:
+ version "2.8.2"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.2.tgz#4126e8c6b107004465918aa2fc35549e77402c87"
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.3.6"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+stream-shift@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
+
+strict-uri-encode@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
+
+string-length@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
+ dependencies:
+ astral-regex "^1.0.0"
+ strip-ansi "^4.0.0"
+
+string-width@^1.0.1, string-width@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string_decoder@^1.0.0, string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-bom@3.0.0, strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-bom@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+ dependencies:
+ is-utf8 "^0.2.0"
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
+strip-indent@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+ dependencies:
+ get-stdin "^4.0.1"
+
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
+style-loader@^0.21.0:
+ version "0.21.0"
+ resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.21.0.tgz#68c52e5eb2afc9ca92b6274be277ee59aea3a852"
+ dependencies:
+ loader-utils "^1.1.0"
+ schema-utils "^0.4.5"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+
+supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
+ dependencies:
+ has-flag "^1.0.0"
+
+supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
+ dependencies:
+ has-flag "^3.0.0"
+
+svgo@^0.7.0:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
+ dependencies:
+ coa "~1.0.1"
+ colors "~1.1.2"
+ csso "~2.3.1"
+ js-yaml "~3.7.0"
+ mkdirp "~0.5.1"
+ sax "~1.2.1"
+ whet.extend "~0.9.9"
+
+symbol-tree@^3.2.1:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
+
+table@4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
+ dependencies:
+ ajv "^5.2.3"
+ ajv-keywords "^2.1.0"
+ chalk "^2.1.0"
+ lodash "^4.17.4"
+ slice-ansi "1.0.0"
+ string-width "^2.1.1"
+
+tapable@^0.2.7, tapable@~0.2.5:
+ version "0.2.8"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
+
+tapable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
+
+tar@^4:
+ version "4.4.3"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.3.tgz#d6bd509dc7f6b5a5d2c13aa0f7d57b03269b8376"
+ dependencies:
+ chownr "^1.0.1"
+ fs-minipass "^1.2.5"
+ minipass "^2.3.3"
+ minizlib "^1.1.0"
+ mkdirp "^0.5.0"
+ safe-buffer "^5.1.2"
+ yallist "^3.0.2"
+
+test-exclude@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.1.tgz#dfa222f03480bca69207ca728b37d74b45f724fa"
+ dependencies:
+ arrify "^1.0.1"
+ micromatch "^3.1.8"
+ object-assign "^4.1.0"
+ read-pkg-up "^1.0.1"
+ require-main-filename "^1.0.1"
+
+text-table@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
+throat@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
+
+through2@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
+ dependencies:
+ readable-stream "^2.1.5"
+ xtend "~4.0.1"
+
+through@^2.3.6:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
+thunky@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.2.tgz#a862e018e3fb1ea2ec3fce5d55605cf57f247371"
+
+time-stamp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357"
+
+timers-browserify@^2.0.4:
+ version "2.0.10"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae"
+ dependencies:
+ setimmediate "^1.0.4"
+
+tmp@^0.0.33:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ dependencies:
+ os-tmpdir "~1.0.2"
+
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+
+to-arraybuffer@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+
+to-fast-properties@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
+toposort@^1.0.0:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
+
+tough-cookie@^2.3.2, tough-cookie@~2.3.3:
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
+ dependencies:
+ punycode "^1.4.1"
+
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+
+trim-newlines@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+tty-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+type-is@~1.6.15, type-is@~1.6.16:
+ version "1.6.16"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.18"
+
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+
+ua-parser-js@^0.7.9:
+ version "0.7.18"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
+
+uglify-es@^3.3.4:
+ version "3.3.9"
+ resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
+ dependencies:
+ commander "~2.13.0"
+ source-map "~0.6.1"
+
+uglify-js@3.3.x:
+ version "3.3.27"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.27.tgz#eb8c3c9429969f86ff5b0a2422ffc78c3cea8cc0"
+ dependencies:
+ commander "~2.15.0"
+ source-map "~0.6.1"
+
+uglify-js@^2.6, uglify-js@^2.8.27:
+ version "2.8.29"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
+ dependencies:
+ source-map "~0.5.1"
+ yargs "~3.10.0"
+ optionalDependencies:
+ uglify-to-browserify "~1.0.0"
+
+uglify-to-browserify@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
+
+uglifyjs-webpack-plugin@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.5.tgz#2ef8387c8f1a903ec5e44fa36f9f3cbdcea67641"
+ dependencies:
+ cacache "^10.0.4"
+ find-cache-dir "^1.0.0"
+ schema-utils "^0.4.5"
+ serialize-javascript "^1.4.0"
+ source-map "^0.6.1"
+ uglify-es "^3.3.4"
+ webpack-sources "^1.1.0"
+ worker-farm "^1.5.2"
+
+underscore@~1.4.4:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"
+
+union-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^0.4.3"
+
+uniq@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+
+uniqs@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+
+unique-filename@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3"
+ dependencies:
+ unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab"
+ dependencies:
+ imurmurhash "^0.1.4"
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+upath@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd"
+
+upper-case@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
+
+uri-js@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.1.tgz#4595a80a51f356164e22970df64c7abd6ade9850"
+ dependencies:
+ punycode "^2.1.0"
+
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+
+url-parse@^1.1.8, url-parse@~1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.0.tgz#6bfdaad60098c7fe06f623e42b22de62de0d3d75"
+ dependencies:
+ querystringify "^2.0.0"
+ requires-port "^1.0.0"
+
+url@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+ dependencies:
+ punycode "1.3.2"
+ querystring "0.2.0"
+
+use@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544"
+ dependencies:
+ kind-of "^6.0.2"
+
+util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+util.promisify@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
+ dependencies:
+ define-properties "^1.1.2"
+ object.getownpropertydescriptors "^2.0.3"
+
+util@0.10.3, util@^0.10.3:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ dependencies:
+ inherits "2.0.1"
+
+utila@~0.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/utila/-/utila-0.3.3.tgz#d7e8e7d7e309107092b05f8d9688824d633a4226"
+
+utila@~0.4:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+
+uuid@^3.0.1, uuid@^3.1.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338"
+ dependencies:
+ spdx-correct "^3.0.0"
+ spdx-expression-parse "^3.0.0"
+
+value-equal@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7"
+
+vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+
+vendors@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.2.tgz#7fcb5eef9f5623b156bcea89ec37d63676f21801"
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+vm-browserify@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
+ dependencies:
+ indexof "0.0.1"
+
+walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ dependencies:
+ makeerror "1.0.x"
+
+watch@~0.18.0:
+ version "0.18.0"
+ resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986"
+ dependencies:
+ exec-sh "^0.2.0"
+ minimist "^1.2.0"
+
+watchpack@^1.3.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
+ dependencies:
+ chokidar "^2.0.2"
+ graceful-fs "^4.1.2"
+ neo-async "^2.5.0"
+
+wbuf@^1.1.0, wbuf@^1.7.2:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
+ dependencies:
+ minimalistic-assert "^1.0.0"
+
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+
+webidl-conversions@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+
+webpack-dev-middleware@1.12.2:
+ version "1.12.2"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e"
+ dependencies:
+ memory-fs "~0.4.1"
+ mime "^1.5.0"
+ path-is-absolute "^1.0.0"
+ range-parser "^1.0.3"
+ time-stamp "^2.0.0"
+
+webpack-dev-server@^2.9.5:
+ version "2.11.2"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.2.tgz#1f4f4c78bf1895378f376815910812daf79a216f"
+ dependencies:
+ ansi-html "0.0.7"
+ array-includes "^3.0.3"
+ bonjour "^3.5.0"
+ chokidar "^2.0.0"
+ compression "^1.5.2"
+ connect-history-api-fallback "^1.3.0"
+ debug "^3.1.0"
+ del "^3.0.0"
+ express "^4.16.2"
+ html-entities "^1.2.0"
+ http-proxy-middleware "~0.17.4"
+ import-local "^1.0.0"
+ internal-ip "1.2.0"
+ ip "^1.1.5"
+ killable "^1.0.0"
+ loglevel "^1.4.1"
+ opn "^5.1.0"
+ portfinder "^1.0.9"
+ selfsigned "^1.9.1"
+ serve-index "^1.7.2"
+ sockjs "0.3.19"
+ sockjs-client "1.1.4"
+ spdy "^3.4.1"
+ strip-ansi "^3.0.0"
+ supports-color "^5.1.0"
+ webpack-dev-middleware "1.12.2"
+ yargs "6.6.0"
+
+webpack-merge@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.2.tgz#5d372dddd3e1e5f8874f5bf5a8e929db09feb216"
+ dependencies:
+ lodash "^4.17.5"
+
+webpack-sources@^1.0.1, webpack-sources@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.6.1"
+
+webpack@^2.0.0:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.7.0.tgz#b2a1226804373ffd3d03ea9c6bd525067034f6b1"
+ dependencies:
+ acorn "^5.0.0"
+ acorn-dynamic-import "^2.0.0"
+ ajv "^4.7.0"
+ ajv-keywords "^1.1.1"
+ async "^2.1.2"
+ enhanced-resolve "^3.3.0"
+ interpret "^1.0.0"
+ json-loader "^0.5.4"
+ json5 "^0.5.1"
+ loader-runner "^2.3.0"
+ loader-utils "^0.2.16"
+ memory-fs "~0.4.1"
+ mkdirp "~0.5.0"
+ node-libs-browser "^2.0.0"
+ source-map "^0.5.3"
+ supports-color "^3.1.0"
+ tapable "~0.2.5"
+ uglify-js "^2.8.27"
+ watchpack "^1.3.1"
+ webpack-sources "^1.0.1"
+ yargs "^6.0.0"
+
+websocket-driver@>=0.5.1:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb"
+ dependencies:
+ http-parser-js ">=0.4.0"
+ websocket-extensions ">=0.1.1"
+
+websocket-extensions@>=0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
+
+whatwg-encoding@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz#57c235bc8657e914d24e1a397d3c82daee0a6ba3"
+ dependencies:
+ iconv-lite "0.4.19"
+
+whatwg-fetch@>=0.10.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
+
+whatwg-url@^4.3.0:
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0"
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
+whet.extend@~0.9.9:
+ version "0.9.9"
+ resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
+
+which-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+
+which@^1.2.12, which@^1.2.9, which@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
+ dependencies:
+ string-width "^1.0.2"
+
+window-size@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+
+wordwrap@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
+
+wordwrap@~0.0.2:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+worker-farm@^1.3.1, worker-farm@^1.5.2:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0"
+ dependencies:
+ errno "~0.1.7"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write-file-atomic@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ signal-exit "^3.0.2"
+
+write@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
+ dependencies:
+ mkdirp "^0.5.1"
+
+xml-name-validator@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
+
+xtend@^4.0.0, xtend@~4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+
+y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+y18n@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+
+yallist@^3.0.0, yallist@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
+
+yargs-parser@^4.2.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c"
+ dependencies:
+ camelcase "^3.0.0"
+
+yargs-parser@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+ dependencies:
+ camelcase "^4.1.0"
+
+yargs@6.6.0, yargs@^6.0.0:
+ version "6.6.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"
+ dependencies:
+ camelcase "^3.0.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^1.4.0"
+ read-pkg-up "^1.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^1.0.2"
+ which-module "^1.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^4.2.0"
+
+yargs@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c"
+ dependencies:
+ camelcase "^4.1.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^2.0.0"
+ read-pkg-up "^2.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^7.0.0"
+
+yargs@~3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
+ dependencies:
+ camelcase "^1.0.2"
+ cliui "^2.1.0"
+ decamelize "^1.0.0"
+ window-size "0.1.0"