Improve routing

This commit is contained in:
Asher 2020-02-04 18:16:45 -06:00
parent dbc5c065f8
commit 8cc11d1688
No known key found for this signature in database
GPG Key ID: D63C1EF81242354A
26 changed files with 289 additions and 267 deletions

View File

@ -22,13 +22,13 @@
"devDependencies": { "devDependencies": {
"@coder/nbin": "^1.2.7", "@coder/nbin": "^1.2.7",
"@types/fs-extra": "^8.0.1", "@types/fs-extra": "^8.0.1",
"@types/hookrouter": "^2.2.1",
"@types/mocha": "^5.2.7", "@types/mocha": "^5.2.7",
"@types/node": "^12.12.7", "@types/node": "^12.12.7",
"@types/parcel-bundler": "^1.12.1", "@types/parcel-bundler": "^1.12.1",
"@types/pem": "^1.9.5", "@types/pem": "^1.9.5",
"@types/react": "^16.9.18", "@types/react": "^16.9.18",
"@types/react-dom": "^16.9.5", "@types/react-dom": "^16.9.5",
"@types/react-router-dom": "^5.1.3",
"@types/safe-compare": "^1.1.0", "@types/safe-compare": "^1.1.0",
"@types/tar-fs": "^1.16.1", "@types/tar-fs": "^1.16.1",
"@types/tar-stream": "^1.6.1", "@types/tar-stream": "^1.6.1",
@ -58,11 +58,11 @@
"dependencies": { "dependencies": {
"@coder/logger": "1.1.11", "@coder/logger": "1.1.11",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"hookrouter": "^1.2.3",
"httpolyglot": "^0.1.2", "httpolyglot": "^0.1.2",
"pem": "^1.14.2", "pem": "^1.14.2",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-router-dom": "^5.1.2",
"safe-compare": "^1.1.4", "safe-compare": "^1.1.4",
"tar-fs": "^2.0.0", "tar-fs": "^2.0.0",
"tar-stream": "^2.1.0", "tar-stream": "^2.1.0",

View File

@ -173,6 +173,21 @@ class Builder {
}) })
await this.copyDependencies("code-server", this.rootPath, this.buildPath) await this.copyDependencies("code-server", this.rootPath, this.buildPath)
await this.task("writing final code-server package.json", async () => {
const json = JSON.parse(await fs.readFile(path.join(this.buildPath, "package.json"), "utf8"))
return fs.writeFile(
path.join(this.buildPath, "package.json"),
JSON.stringify(
{
...json,
commit,
},
null,
2
)
)
})
} }
private async buildVscode(commit: string): Promise<void> { private async buildVscode(commit: string): Promise<void> {
@ -369,6 +384,9 @@ class Builder {
bundler.on("buildEnd", () => { bundler.on("buildEnd", () => {
console.log("[parcel] bundled") console.log("[parcel] bundled")
}) })
bundler.on("buildError", (error) => {
console.error("[parcel]", error)
})
vscode.stderr.on("data", (d) => process.stderr.write(d)) vscode.stderr.on("data", (d) => process.stderr.write(d))
tsc.stderr.on("data", (d) => process.stderr.write(d)) tsc.stderr.on("data", (d) => process.stderr.write(d))

View File

@ -12,7 +12,12 @@ iframe {
body { body {
background: #272727; background: #272727;
color: #f4f4f4;
margin: 0; margin: 0;
font-family: 'IBM Plex Sans', sans-serif; font-family: 'IBM Plex Sans', sans-serif;
overflow: hidden; overflow: hidden;
} }
button {
font-family: inherit;
}

View File

@ -1,35 +1,40 @@
import { getBasepath, navigate } from "hookrouter"
import * as React from "react" import * as React from "react"
import { Application } from "../common/api" import { Application, isExecutableApplication } from "../common/api"
import { Route, Switch } from "react-router-dom"
import { HttpError } from "../common/http" import { HttpError } from "../common/http"
import { normalize, Options } from "../common/util"
import { Modal } from "./components/modal" import { Modal } from "./components/modal"
import { getOptions } from "../common/util"
const App: React.FunctionComponent = () => { export interface AppProps {
const [authed, setAuthed] = React.useState<boolean>(false) options: Options
const [app, setApp] = React.useState<Application>() }
const App: React.FunctionComponent<AppProps> = (props) => {
const [authed, setAuthed] = React.useState<boolean>(!!props.options.authed)
const [app, setApp] = React.useState<Application | undefined>(props.options.app)
const [error, setError] = React.useState<HttpError | Error | string>() const [error, setError] = React.useState<HttpError | Error | string>()
React.useEffect(() => { React.useEffect(() => {
getOptions() if (app && !isExecutableApplication(app)) {
}, []) navigate(normalize(`${getBasepath()}/${app.path}/`, true))
}
}, [app])
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).setAuthed = setAuthed ;(window as any).setAuthed = (a: boolean): void => {
if (authed !== a) {
setAuthed(a)
}
}
} }
return ( return (
<> <>
<Switch>
<Route path="/vscode" render={(): React.ReactElement => <iframe id="iframe" src="/vscode-embed"></iframe>} />
<Route
path="/"
render={(): React.ReactElement => (
<Modal app={app} setApp={setApp} authed={authed} error={error} setError={setError} /> <Modal app={app} setApp={setApp} authed={authed} error={error} setError={setError} />
)} {authed && app && app.embedPath ? (
/> <iframe id="iframe" src={normalize(`${getBasepath()}/${app.embedPath}/`, true)}></iframe>
</Switch> ) : null}
</> </>
) )
} }

View File

@ -5,6 +5,10 @@ export interface DelayProps {
readonly delay: number readonly delay: number
} }
/**
* Animate a component before unmounting (by delaying unmounting) or after
* mounting.
*/
export const Animate: React.FunctionComponent<DelayProps> = (props) => { export const Animate: React.FunctionComponent<DelayProps> = (props) => {
const [timer, setTimer] = React.useState<NodeJS.Timeout>() const [timer, setTimer] = React.useState<NodeJS.Timeout>()
const [mount, setMount] = React.useState<boolean>(false) const [mount, setMount] = React.useState<boolean>(false)

View File

@ -4,6 +4,7 @@ import { HttpError } from "../../common/http"
export interface ErrorProps { export interface ErrorProps {
error: HttpError | Error | string error: HttpError | Error | string
onClose?: () => void onClose?: () => void
onCloseText?: string
} }
/** /**
@ -15,7 +16,7 @@ export const RequestError: React.FunctionComponent<ErrorProps> = (props) => {
<div className="error">{typeof props.error === "string" ? props.error : props.error.message}</div> <div className="error">{typeof props.error === "string" ? props.error : props.error.message}</div>
{props.onClose ? ( {props.onClose ? (
<button className="close" onClick={props.onClose}> <button className="close" onClick={props.onClose}>
Go Back {props.onCloseText || "Close"}
</button> </button>
) : ( ) : (
undefined undefined

View File

@ -20,7 +20,6 @@
.app-row { .app-row {
color: #b6b6b6; color: #b6b6b6;
cursor: pointer;
display: flex; display: flex;
font-size: 1em; font-size: 1em;
line-height: 1em; line-height: 1em;
@ -106,3 +105,7 @@
.app-list > .loader { .app-list > .loader {
color: #b6b6b6; color: #b6b6b6;
} }
.app-list > .app-row {
cursor: pointer;
}

View File

@ -4,6 +4,9 @@ import { HttpError } from "../../common/http"
import { getSession, killSession } from "../api" import { getSession, killSession } from "../api"
import { RequestError } from "../components/error" import { RequestError } from "../components/error"
/**
* An application's details (name and icon).
*/
export const AppDetails: React.FunctionComponent<Application> = (props) => { export const AppDetails: React.FunctionComponent<Application> = (props) => {
return ( return (
<> <>
@ -23,6 +26,9 @@ export interface AppRowProps {
open(app: Application): void open(app: Application): void
} }
/**
* A single application row. Can be killed if it's a running application.
*/
export const AppRow: React.FunctionComponent<AppRowProps> = (props) => { export const AppRow: React.FunctionComponent<AppRowProps> = (props) => {
const [killing, setKilling] = React.useState<boolean>(false) const [killing, setKilling] = React.useState<boolean>(false)
const [error, setError] = React.useState<HttpError>() const [error, setError] = React.useState<HttpError>()
@ -65,6 +71,11 @@ export interface AppListProps {
onKilled(app: Application): void onKilled(app: Application): void
} }
/**
* A list of applications. If undefined, show loading text. If empty, show a
* message saying no items are found. Applications can be clicked and killed
* (when applicable).
*/
export const AppList: React.FunctionComponent<AppListProps> = (props) => { export const AppList: React.FunctionComponent<AppListProps> = (props) => {
return ( return (
<div className="app-list"> <div className="app-list">
@ -92,11 +103,12 @@ export interface AppLoaderProps {
} }
/** /**
* Display provided applications or sessions and allow opening them. * Application sections/groups. Handles loading of the application
* sections, errors, opening applications, and killing applications.
*/ */
export const AppLoader: React.FunctionComponent<AppLoaderProps> = (props) => { export const AppLoader: React.FunctionComponent<AppLoaderProps> = (props) => {
const [apps, setApps] = React.useState<ReadonlyArray<ApplicationSection>>() const [apps, setApps] = React.useState<ReadonlyArray<ApplicationSection>>()
const [error, setError] = React.useState<HttpError>() const [error, setError] = React.useState<HttpError | Error>()
const refresh = (): void => { const refresh = (): void => {
props props
@ -105,33 +117,51 @@ export const AppLoader: React.FunctionComponent<AppLoaderProps> = (props) => {
.catch((e) => setError(e.message)) .catch((e) => setError(e.message))
} }
// Every time the component loads go ahead and refresh the list.
React.useEffect(() => { React.useEffect(() => {
refresh() refresh()
}, [props]) }, [props])
/**
* Open an application if not already open. For executable applications create
* a session first.
*/
function open(app: Application): void { function open(app: Application): void {
if (props.app && props.app.name === app.name) {
return setError(new Error(`${app.name} is already open`))
}
props.setApp(app) props.setApp(app)
if (!isRunningApplication(app) && isExecutableApplication(app)) { if (!isRunningApplication(app) && isExecutableApplication(app)) {
getSession(app) getSession(app)
.then((session) => { .then((session) => {
props.setApp({ ...app, ...session }) props.setApp({ ...app, ...session })
}) })
.catch(setError) .catch((error) => {
props.setApp(undefined)
setError(error)
})
} }
} }
// In the case of an error fetching the apps, have the ability to try again.
// In the case of failing to load an app, have the ability to go back to the
// list (where the user can try again if they wish).
if (error) { if (error) {
props.setApp(undefined)
return ( return (
<RequestError <RequestError
error={error} error={error}
onCloseText={props.app ? "Go Back" : "Try Again"}
onClose={(): void => { onClose={(): void => {
setError(undefined) setError(undefined)
if (!props.app) {
refresh()
}
}} }}
/> />
) )
} }
// If an app is currently loading, provide the option to cancel.
if (props.app && !props.app.loaded) { if (props.app && !props.app.loaded) {
return ( return (
<div className="app-loader"> <div className="app-loader">
@ -151,14 +181,16 @@ export const AppLoader: React.FunctionComponent<AppLoaderProps> = (props) => {
) )
} }
// Apps are currently loading.
if (!apps) { if (!apps) {
return ( return (
<div className="app-loader"> <div className="app-loader">
<div className="loader">loading</div> <div className="loader">loading...</div>
</div> </div>
) )
} }
// Apps have loaded.
return ( return (
<> <>
{apps.map((section, i) => ( {apps.map((section, i) => (

View File

@ -100,19 +100,22 @@
width: 100%; width: 100%;
} }
.modal-container > .modal > .sidebar { .sidebar-nav {
border-right: 1.5px solid rgba(0, 0, 0, 0.37);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
min-width: 145px;
} }
.modal-container > .modal > .sidebar > .links { .sidebar-nav > .links {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.modal-container > .modal > .sidebar > .links > .link { .sidebar-nav > .links > .link {
background-color: transparent;
border: none;
cursor: pointer;
color: rgba(0, 0, 0, 0.37); color: rgba(0, 0, 0, 0.37);
font-size: 1.4em; font-size: 1.4em;
height: 31px; height: 31px;
@ -122,7 +125,7 @@
transition: 150ms color ease, 150ms height ease, 150ms margin-bottom ease; transition: 150ms color ease, 150ms height ease, 150ms margin-bottom ease;
} }
.modal-container > .modal > .sidebar > .footer > .close { .sidebar-nav > .footer > .close {
background: transparent; background: transparent;
border: none; border: none;
color: #b6b6b6; color: #b6b6b6;
@ -130,14 +133,12 @@
width: 100%; width: 100%;
} }
.modal-container > .modal > .sidebar > .footer > .close:hover { .sidebar-nav > .links > .link[aria-current="page"],
.sidebar-nav > .links > .link:hover,
.sidebar-nav > .footer > .close:hover {
color: #000; color: #000;
} }
.modal-container > .modal > .links > .link[aria-current="page"] {
color: rgba(0, 0, 0, 1);
}
.modal-container > .modal > .content { .modal-container > .modal > .content {
display: flex; display: flex;
flex: 1; flex: 1;
@ -145,3 +146,7 @@
overflow: auto; overflow: auto;
padding: 0 20px; padding: 0 20px;
} }
.modal-container > .modal > .sidebar-nav {
border-right: 1.5px solid rgba(0, 0, 0, 0.37);
}

View File

@ -1,12 +1,12 @@
import { logger } from "@coder/logger" import { logger } from "@coder/logger"
import * as React from "react" import * as React from "react"
import { NavLink, Route, RouteComponentProps, Switch } from "react-router-dom"
import { Application, isExecutableApplication } from "../../common/api" import { Application, isExecutableApplication } from "../../common/api"
import { HttpError } from "../../common/http" import { HttpError } from "../../common/http"
import { RequestError } from "../components/error" import { RequestError } from "../components/error"
import { Browse } from "../pages/browse" import { Browse } from "../pages/browse"
import { Home } from "../pages/home" import { Home } from "../pages/home"
import { Login } from "../pages/login" import { Login } from "../pages/login"
import { Missing } from "../pages/missing"
import { Open } from "../pages/open" import { Open } from "../pages/open"
import { Recent } from "../pages/recent" import { Recent } from "../pages/recent"
import { Animate } from "./animate" import { Animate } from "./animate"
@ -19,9 +19,18 @@ export interface ModalProps {
setError(error?: HttpError | Error | string): void setError(error?: HttpError | Error | string): void
} }
enum Section {
Browse,
Home,
Login,
Open,
Recent,
}
export const Modal: React.FunctionComponent<ModalProps> = (props) => { export const Modal: React.FunctionComponent<ModalProps> = (props) => {
const [showModal, setShowModal] = React.useState<boolean>(false) const [showModal, setShowModal] = React.useState<boolean>(false)
const [showBar, setShowBar] = React.useState<boolean>(true) const [showBar, setShowBar] = React.useState<boolean>(false) // TEMP: Will be true.
const [section, setSection] = React.useState<Section>(Section.Home)
const setApp = (app: Application): void => { const setApp = (app: Application): void => {
setShowModal(false) setShowModal(false)
@ -36,7 +45,8 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
clearTimeout(timeout) clearTimeout(timeout)
timeout = undefined timeout = undefined
} else if (clientY <= 30 && !timeout) { } else if (clientY <= 30 && !timeout) {
timeout = setTimeout(() => setShowBar(true), 1000) // TEMP: No bar for now.
// timeout = setTimeout(() => setShowBar(true), 1000)
} }
} }
@ -91,39 +101,49 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
} }
}, [showBar, props.error, showModal, props.app]) }, [showBar, props.error, showModal, props.app])
const content = (): React.ReactElement => {
if (!props.authed) {
return <Login />
}
switch (section) {
case Section.Recent:
return <Recent app={props.app} setApp={setApp} />
case Section.Home:
return <Home app={props.app} />
case Section.Browse:
return <Browse />
case Section.Login:
return <Login />
case Section.Open:
return <Open app={props.app} setApp={setApp} />
default:
return <Missing />
}
}
return props.error || showModal || !props.app || !props.app.loaded ? ( return props.error || showModal || !props.app || !props.app.loaded ? (
<div className="modal-container"> <div className="modal-container">
<div className="modal"> <div className="modal">
{props.authed && (!props.app || props.app.loaded) ? ( {props.authed && (!props.app || props.app.loaded) ? (
<aside className="sidebar"> <aside className="sidebar-nav">
<nav className="links"> <nav className="links">
{!props.authed ? (
<NavLink className="link" to="/login">
Login
</NavLink>
) : (
undefined
)}
{props.authed ? ( {props.authed ? (
<NavLink className="link" exact to="/recent/"> // TEMP: Remove once we don't auto-load vscode.
<>
<button className="link" onClick={(): void => setSection(Section.Recent)}>
Recent Recent
</NavLink> </button>
) : ( <button className="link" onClick={(): void => setSection(Section.Open)}>
undefined
)}
{props.authed ? (
<NavLink className="link" exact to="/open/">
Open Open
</NavLink> </button>
) : ( <button className="link" onClick={(): void => setSection(Section.Browse)}>
undefined
)}
{props.authed ? (
<NavLink className="link" exact to="/browse/">
Browse Browse
</NavLink> </button>
</>
) : ( ) : (
undefined <button className="link" onClick={(): void => setSection(Section.Login)}>
Login
</button>
)} )}
</nav> </nav>
<div className="footer"> <div className="footer">
@ -148,23 +168,7 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
}} }}
/> />
) : ( ) : (
<div className="content"> <div className="content">{content()}</div>
<Switch>
<Route path="/login" component={Login} />
<Route
path="/recent"
render={(p: RouteComponentProps): React.ReactElement => (
<Recent app={props.app} setApp={setApp} {...p} />
)}
/>
<Route path="/browse" component={Browse} />
<Route
path="/open"
render={(p: RouteComponentProps): React.ReactElement => <Open app={props.app} setApp={setApp} {...p} />}
/>
<Route path="/" component={Home} />
</Switch>
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -1,18 +1,14 @@
import * as React from "react" import * as React from "react"
import * as ReactDOM from "react-dom" import * as ReactDOM from "react-dom"
import App from "./app" import App from "./app"
import { BrowserRouter } from "react-router-dom" import { getOptions } from "../common/util"
import "./app.css" import "./app.css"
import "./pages/home.css" import "./pages/home.css"
import "./pages/login.css" import "./pages/login.css"
import "./pages/missing.css"
import "./components/error.css" import "./components/error.css"
import "./components/list.css" import "./components/list.css"
import "./components/modal.css" import "./components/modal.css"
ReactDOM.hydrate( ReactDOM.hydrate(<App options={getOptions()} />, document.getElementById("root"))
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
)

View File

@ -1,5 +1,4 @@
import * as React from "react" import * as React from "react"
import { RouteComponentProps } from "react-router"
import { FilesResponse } from "../../common/api" import { FilesResponse } from "../../common/api"
import { HttpError } from "../../common/http" import { HttpError } from "../../common/http"
import { getFiles } from "../api" import { getFiles } from "../api"
@ -8,14 +7,14 @@ import { RequestError } from "../components/error"
/** /**
* File browser. * File browser.
*/ */
export const Browse: React.FunctionComponent<RouteComponentProps> = (props) => { export const Browse: React.FunctionComponent = (props) => {
const [response, setResponse] = React.useState<FilesResponse>() const [response, setResponse] = React.useState<FilesResponse>()
const [error, setError] = React.useState<HttpError>() const [error, setError] = React.useState<HttpError>()
React.useEffect(() => { React.useEffect(() => {
getFiles() getFiles()
.then(setResponse) .then(setResponse)
.catch((e) => setError(e.message)) .catch(setError)
}, [props]) }, [props])
return ( return (

View File

@ -1,22 +1,20 @@
import * as React from "react" import * as React from "react"
import { RouteComponentProps } from "react-router" import { Application } from "../../common/api"
import { authenticate } from "../api" import { authenticate } from "../api"
export const Home: React.FunctionComponent<RouteComponentProps> = (props) => { export interface HomeProps {
app?: Application
}
export const Home: React.FunctionComponent<HomeProps> = (props) => {
React.useEffect(() => { React.useEffect(() => {
authenticate() authenticate().catch(() => undefined)
.then(() => {
// TEMP: Always redirect to VS Code.
props.history.push("./vscode/")
})
.catch(() => {
props.history.push("./login/")
})
}, []) }, [])
return ( return (
<div className="orientation-guide"> <div className="orientation-guide">
<div className="welcome">Welcome to code-server.</div> <div className="welcome">Welcome to code-server.</div>
{props.app && !props.app.loaded ? <div className="loader">loading...</div> : undefined}
</div> </div>
) )
} }

View File

@ -4,9 +4,7 @@
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
font-weight: 700;
justify-content: center; justify-content: center;
text-transform: uppercase;
} }
.login-form > .field { .login-form > .field {
@ -16,13 +14,13 @@
} }
.login-form > .field-error { .login-form > .field-error {
margin-top: 10px; margin-top: 1em;
} }
.login-form > .field > .input { .login-form > .field > .input {
border: 1px solid #b6b6b6; border: 1px solid #b6b6b6;
box-sizing: border-box; box-sizing: border-box;
padding: 10px; padding: 1em;
flex: 1; flex: 1;
} }
@ -31,5 +29,11 @@
border: 1px solid #b6b6b6; border: 1px solid #b6b6b6;
box-sizing: border-box; box-sizing: border-box;
margin-left: -1px; margin-left: -1px;
padding: 10px 20px; padding: 1em 2em;
}
.login-header {
align-items: center;
color: #b6b6b6;
margin-bottom: 1em;
} }

View File

@ -1,5 +1,4 @@
import * as React from "react" import * as React from "react"
import { RouteComponentProps } from "react-router"
import { HttpError } from "../../common/http" import { HttpError } from "../../common/http"
import { authenticate } from "../api" import { authenticate } from "../api"
import { FieldError } from "../components/error" import { FieldError } from "../components/error"
@ -7,35 +6,25 @@ import { FieldError } from "../components/error"
/** /**
* Login page. Will redirect on success. * Login page. Will redirect on success.
*/ */
export const Login: React.FunctionComponent<RouteComponentProps> = (props) => { export const Login: React.FunctionComponent = () => {
const [password, setPassword] = React.useState<string>("") const [password, setPassword] = React.useState<string>("")
const [error, setError] = React.useState<HttpError>() const [error, setError] = React.useState<HttpError>()
function redirect(): void {
// TEMP: Always redirect to VS Code.
console.log("is authed")
props.history.push("../vscode/")
// const params = new URLSearchParams(window.location.search)
// props.history.push(params.get("to") || "/")
}
async function handleSubmit(event: React.FormEvent<HTMLFormElement>): Promise<void> { async function handleSubmit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault() event.preventDefault()
authenticate({ password }) authenticate({ password }).catch(setError)
.then(redirect)
.catch(setError)
} }
React.useEffect(() => { React.useEffect(() => {
authenticate() authenticate().catch(() => undefined)
.then(redirect)
.catch(() => {
// Do nothing; we're already at the login page.
})
}, []) }, [])
return ( return (
<form className="login-form" onSubmit={handleSubmit}> <form className="login-form" onSubmit={handleSubmit}>
<div className="login-header">
<div className="main">Welcome to code-server</div>
<div className="sub">Please log in below</div>
</div>
<div className="field"> <div className="field">
<input <input
autoFocus autoFocus

View File

@ -0,0 +1,8 @@
.missing-message {
align-items: center;
color: #b6b6b6;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
}

View File

@ -0,0 +1,9 @@
import * as React from "react"
export const Missing: React.FunctionComponent = () => {
return (
<div className="missing-message">
<div className="message">404</div>
</div>
)
}

View File

@ -1,6 +1,7 @@
export interface Application { export interface Application {
readonly comment?: string readonly comment?: string
readonly directory?: string readonly directory?: string
readonly embedPath?: string
readonly exec?: string readonly exec?: string
readonly icon?: string readonly icon?: string
readonly loaded?: boolean readonly loaded?: boolean

View File

@ -1,6 +1,9 @@
import { logger } from "@coder/logger" import { logger } from "@coder/logger"
import { Application } from "../common/api"
export interface Options { export interface Options {
app?: Application
authed?: boolean
logLevel?: number logLevel?: number
} }
@ -27,6 +30,9 @@ export const generateUuid = (length = 24): string => {
* Get options embedded in the HTML from the server. * Get options embedded in the HTML from the server.
*/ */
export const getOptions = <T extends Options>(): T => { export const getOptions = <T extends Options>(): T => {
if (typeof document === "undefined") {
return {} as T
}
const el = document.getElementById("coder-options") const el = document.getElementById("coder-options")
try { try {
if (!el) { if (!el) {
@ -46,3 +52,10 @@ export const getOptions = <T extends Options>(): T => {
return {} as T return {} as T
} }
} }
/**
* Remove extra slashes in a URL.
*/
export const normalize = (url: string, keepTrailing = false): string => {
return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "")
}

View File

@ -151,6 +151,7 @@ export class ApiHttpProvider extends HttpProvider {
{ {
name: "VS Code", name: "VS Code",
path: "/vscode", path: "/vscode",
embedPath: "/vscode-embed",
}, },
], ],
}, },

View File

@ -1,33 +1,49 @@
import { logger } from "@coder/logger" import { logger } from "@coder/logger"
import * as http from "http"
import * as querystring from "querystring"
import * as React from "react" import * as React from "react"
import * as ReactDOMServer from "react-dom/server" import * as ReactDOMServer from "react-dom/server"
import * as ReactRouterDOM from "react-router-dom"
import App from "../../browser/app" import App from "../../browser/app"
import { Options } from "../../common/util"
import { HttpProvider, HttpResponse } from "../http" import { HttpProvider, HttpResponse } from "../http"
/** /**
* Top-level and fallback HTTP provider. * Top-level and fallback HTTP provider.
*/ */
export class MainHttpProvider extends HttpProvider { export class MainHttpProvider extends HttpProvider {
public async handleRequest(base: string, requestPath: string): Promise<HttpResponse | undefined> { public async handleRequest(
base: string,
requestPath: string,
_query: querystring.ParsedUrlQuery,
request: http.IncomingMessage
): Promise<HttpResponse | undefined> {
if (base === "/static") { if (base === "/static") {
const response = await this.getResource(this.rootPath, requestPath) const response = await this.getResource(this.rootPath, requestPath)
if (this.options.commit && this.options.commit !== "development") {
response.cache = true response.cache = true
}
return response return response
} }
// TEMP: Auto-load VS Code for now. In future versions we'll need to check
// the URL for the appropriate application to load, if any.
const app = {
name: "VS Code",
path: "/",
embedPath: "/vscode-embed",
}
const options: Options = {
app,
authed: !!this.authenticated(request),
logLevel: logger.level,
}
const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html") const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html")
response.content = response.content response.content = response.content
.replace(/{{COMMIT}}/g, "") // TODO .replace(/{{COMMIT}}/g, this.options.commit || "development")
.replace(/"{{OPTIONS}}"/g, `'${JSON.stringify({ logLevel: logger.level })}'`) .replace(/"{{OPTIONS}}"/g, `'${JSON.stringify(options)}'`)
.replace( .replace(/{{COMPONENT}}/g, ReactDOMServer.renderToString(<App options={options} />))
/{{COMPONENT}}/g,
ReactDOMServer.renderToString(
<ReactRouterDOM.StaticRouter location={base}>
<App />
</ReactRouterDOM.StaticRouter>
)
)
return response return response
} }

View File

@ -23,25 +23,32 @@ const main = async (args: Args = {}): Promise<void> => {
const auth = args.auth || AuthType.Password const auth = args.auth || AuthType.Password
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword())) const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
let commit = "development"
try {
commit = require("../../package.json").commit
} catch (error) {
logger.warn(error.message)
}
// Spawn the main HTTP server. // Spawn the main HTTP server.
const options = { const options = {
auth,
basePath: args["base-path"], basePath: args["base-path"],
cert: args.cert, cert: args.cert,
certKey: args["cert-key"], certKey: args["cert-key"],
commit,
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"), host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
password: originalPassword ? hash(originalPassword) : undefined,
port: typeof args.port !== "undefined" ? parseInt(args.port, 10) : 8080, port: typeof args.port !== "undefined" ? parseInt(args.port, 10) : 8080,
socket: args.socket, socket: args.socket,
auth,
password: originalPassword ? hash(originalPassword) : undefined,
} }
if (!options.cert && typeof options.cert !== "undefined") { if (!options.cert && typeof options.cert !== "undefined") {
const { cert, certKey } = await generateCertificate() const { cert, certKey } = await generateCertificate()
options.cert = cert options.cert = cert
options.certKey = certKey options.certKey = certKey
} }
const httpServer = new HttpServer(options)
// Register all the providers. const httpServer = new HttpServer(options)
httpServer.registerHttpProvider("/", MainHttpProvider) httpServer.registerHttpProvider("/", MainHttpProvider)
httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer) httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer)
httpServer.registerHttpProvider("/vscode-embed", VscodeHttpProvider, []) httpServer.registerHttpProvider("/vscode-embed", VscodeHttpProvider, [])

View File

@ -12,8 +12,8 @@ import * as tarFs from "tar-fs"
import * as tls from "tls" import * as tls from "tls"
import * as url from "url" import * as url from "url"
import { HttpCode, HttpError } from "../common/http" import { HttpCode, HttpError } from "../common/http"
import { plural, split } from "../common/util" import { normalize, plural, split } from "../common/util"
import { getMediaMime, normalize, xdgLocalDir } from "./util" import { getMediaMime, xdgLocalDir } from "./util"
export type Cookies = { [key: string]: string[] | undefined } export type Cookies = { [key: string]: string[] | undefined }
export type PostData = { [key: string]: string | string[] | undefined } export type PostData = { [key: string]: string | string[] | undefined }
@ -92,6 +92,7 @@ export interface HttpServerOptions {
readonly basePath?: string readonly basePath?: string
readonly cert?: string readonly cert?: string
readonly certKey?: string readonly certKey?: string
readonly commit?: string
readonly host?: string readonly host?: string
readonly password?: string readonly password?: string
readonly port?: number readonly port?: number
@ -111,6 +112,7 @@ export interface HttpProviderOptions {
readonly base: string readonly base: string
readonly auth: AuthType readonly auth: AuthType
readonly password?: string readonly password?: string
readonly commit?: string
} }
/** /**
@ -120,7 +122,7 @@ export interface HttpProviderOptions {
export abstract class HttpProvider { export abstract class HttpProvider {
protected readonly rootPath = path.resolve(__dirname, "../..") protected readonly rootPath = path.resolve(__dirname, "../..")
public constructor(private readonly options: HttpProviderOptions) {} public constructor(protected readonly options: HttpProviderOptions) {}
public dispose(): void { public dispose(): void {
// No default behavior. // No default behavior.
@ -403,6 +405,7 @@ export class HttpServer {
{ {
auth: this.options.auth || AuthType.None, auth: this.options.auth || AuthType.None,
base: endpoint, base: endpoint,
commit: this.options.commit,
password: this.options.password, password: this.options.password,
}, },
a1 a1

View File

@ -207,10 +207,3 @@ export function extend(...args: any[]): any {
} }
return c return c
} }
/**
* Remove extra and trailing slashes in a URL.
*/
export const normalize = (url: string): string => {
return url.replace(/\/\/+/g, "/").replace(/\/+$/, "")
}

View File

@ -1,5 +1,6 @@
import * as assert from "assert" import * as assert from "assert"
import { extend, normalize } from "../src/node/util" import { normalize } from "../src/common/util"
import { extend } from "../src/node/util"
describe("util", () => { describe("util", () => {
describe("extend", () => { describe("extend", () => {
@ -45,5 +46,10 @@ describe("util", () => {
it("should remove trailing slashes", () => { it("should remove trailing slashes", () => {
assert.equal(normalize("qux///"), "qux") assert.equal(normalize("qux///"), "qux")
}) })
it("should preserve trailing slash if it exists", () => {
assert.equal(normalize("qux///", true), "qux/")
assert.equal(normalize("qux", true), "qux")
})
}) })
}) })

126
yarn.lock
View File

@ -714,7 +714,7 @@
levenary "^1.1.1" levenary "^1.1.1"
semver "^5.5.0" semver "^5.5.0"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.6.3": "@babel/runtime@^7.4.4", "@babel/runtime@^7.6.3":
version "7.8.4" version "7.8.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308"
integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==
@ -881,10 +881,13 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/history@*": "@types/hookrouter@^2.2.1":
version "4.7.5" version "2.2.1"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860" resolved "https://registry.yarnpkg.com/@types/hookrouter/-/hookrouter-2.2.1.tgz#0c69e671957b48ade9e042612faf3fc833a3fd59"
integrity sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw== integrity sha512-C4Ae6yf8vE4TEKZa0EpP2o85UMVHKsZeJwT0oWiQ1QkNBbkQ8bk5A+qWS25Uect9r1Uivz/dnSg6v4WwzFjgrg==
dependencies:
"@types/react" "*"
csstype "^2.2.0"
"@types/json-schema@^7.0.3": "@types/json-schema@^7.0.3":
version "7.0.4" version "7.0.4"
@ -952,23 +955,6 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-router-dom@^5.1.3":
version "5.1.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196"
integrity sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA==
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react-router" "*"
"@types/react-router@*":
version "5.1.4"
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.4.tgz#7d70bd905543cb6bcbdcc6bd98902332054f31a6"
integrity sha512-PZtnBuyfL07sqCJvGg3z+0+kt6fobc/xmle08jBiezLS8FrmGeiGkJnuxL/8Zgy9L83ypUhniV5atZn/L8n9MQ==
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react@*", "@types/react@^16.9.18": "@types/react@*", "@types/react@^16.9.18":
version "16.9.19" version "16.9.19"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.19.tgz#c842aa83ea490007d29938146ff2e4d9e4360c40" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.19.tgz#c842aa83ea490007d29938146ff2e4d9e4360c40"
@ -3460,11 +3446,6 @@ growl@1.10.5:
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
gud@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
har-schema@^2.0.0: har-schema@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@ -3574,18 +3555,6 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
dependencies:
"@babel/runtime" "^7.1.2"
loose-envify "^1.2.0"
resolve-pathname "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^1.0.1"
hmac-drbg@^1.0.0: hmac-drbg@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@ -3595,12 +3564,10 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1" minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.1.0: hookrouter@^1.2.3:
version "3.3.2" version "1.2.3"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hookrouter/-/hookrouter-1.2.3.tgz#a65599a1be376b51734caf7c4f7f8aba59bb2c77"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== integrity sha512-n0mqEBGgXIxYRNMHlwCzoyTOk0OB5Es3jwUyA3+2l5nte/52n0CMMj1bmoCabC8K43YTUEr0zzexTBfo//tq2Q==
dependencies:
react-is "^16.7.0"
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.8.5" version "2.8.5"
@ -4461,7 +4428,7 @@ longest-streak@^2.0.1:
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4"
integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -4643,15 +4610,6 @@ min-indent@^1.0.0:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256"
integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY= integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=
mini-create-react-context@^0.3.0:
version "0.3.2"
resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189"
integrity sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==
dependencies:
"@babel/runtime" "^7.4.0"
gud "^1.0.0"
tiny-warning "^1.0.2"
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
@ -5342,13 +5300,6 @@ path-platform@~0.11.15:
resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2"
integrity sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I= integrity sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^2.0.0: path-type@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
@ -6031,40 +5982,11 @@ react-dom@^16.12.0:
prop-types "^15.6.2" prop-types "^15.6.2"
scheduler "^0.18.0" scheduler "^0.18.0"
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: react-is@^16.8.1:
version "16.12.0" version "16.12.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
react-router-dom@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.1.2"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418"
integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
mini-create-react-context "^0.3.0"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react@^16.12.0: react@^16.12.0:
version "16.12.0" version "16.12.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83" resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83"
@ -6402,11 +6324,6 @@ resolve-from@^5.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve-url@^0.2.1: resolve-url@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@ -7321,16 +7238,6 @@ tiny-inflate@^1.0.0:
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
tiny-invariant@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-warning@^1.0.0, tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@ -7763,11 +7670,6 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0" spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0" spdx-expression-parse "^3.0.0"
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
vendors@^1.0.0: vendors@^1.0.0:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"