Add external redirect infrastructure (#550)

Summary:
This patch extends our routing infrastructure to add support for
_external_ redirects. It does not include dedicated support for
site-internal redirects.

Test Plan:
Add an external redirect to `routeData`, like the following:

```diff
diff --git a/src/app/routeData.js b/src/app/routeData.js
index 83dff72..eaba130 100644
--- a/src/app/routeData.js
+++ b/src/app/routeData.js
@@ -36,6 +36,15 @@ const routeData /*: $ReadOnlyArray<RouteDatum> */ = [
     title: "SourceCred explorer",
     navTitle: "Explorer",
   },
+  {
+    path: "/discord-invite",
+    contents: {
+      type: "EXTERNAL_REDIRECT",
+      redirectTo: "https://discord.gg/tsBTgc9",
+    },
+    title: "SourceCred Discord invite",
+    navTitle: null,
+  },
 ];
 exports.routeData = routeData;

```

Then:
  - run `yarn build`, and:
     - verify that the appropriate `index.html` file is correctly
       generated;
     - verify that opening the `index.html` file in a browser redirects
       you to the appropriate destination, even with JavaScript
       disabled;
     - verify that the link in the body of the HTML page is correct
       (easier to do if you remove the `<meta>` tag)
  - run `yarn start`, and:
    1. use the React DevTools to change the “Explorer” link’s `to` prop
       from `/explorer` to `/discord-invite`;
    2. click the link; and
    3. verify that you are properly redirected.

wchargin-branch: add-external-redirect
This commit is contained in:
William Chargin 2018-07-27 19:13:51 -07:00 committed by GitHub
parent 24a950629a
commit 8e062592ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 141 additions and 42 deletions

View File

@ -0,0 +1,26 @@
// @flow
import React from "react";
export default class ExternalRedirect extends React.Component<{|
+redirectTo: string,
|}> {
render() {
return (
<React.Fragment>
<h1>Redirecting</h1>
<p>
Redirecting to:{" "}
<a href={this.props.redirectTo}>{this.props.redirectTo}</a>
</p>
</React.Fragment>
);
}
componentDidMount() {
// The server-rendered copy of this page will have a meta-refresh
// tag, but someone could still plausibly navigate to this page with
// the client-side renderer. In that case, we should redirect them.
window.location.href = this.props.redirectTo;
}
}

View File

@ -4,16 +4,38 @@ import React from "react";
import {IndexRoute, Route} from "react-router";
import Page from "./Page";
import ExternalRedirect from "./ExternalRedirect";
import {routeData} from "./routeData";
export function createRoutes() {
return (
<Route path="/" component={Page}>
{routeData.map(({path, component}) => {
if (path === "/") {
return <IndexRoute key={path} component={component()} />;
} else {
return <Route key={path} path={path} component={component()} />;
{routeData.map(({path, contents}) => {
switch (contents.type) {
case "PAGE":
if (path === "/") {
return <IndexRoute key={path} component={contents.component()} />;
} else {
return (
<Route
key={path}
path={path}
component={contents.component()}
/>
);
}
case "EXTERNAL_REDIRECT":
return (
<Route
key={path}
path={path}
component={() => (
<ExternalRedirect redirectTo={contents.redirectTo} />
)}
/>
);
default:
throw new Error((contents.type: empty));
}
})}
</Route>

View File

@ -9,7 +9,9 @@
/*::
type RouteDatum = {|
+path: string,
+component: () => React$ComponentType<{||}>,
+contents:
| {|+type: "PAGE", +component: () => React$ComponentType<{||}>|}
| {|+type: "EXTERNAL_REDIRECT", +redirectTo: string|},
+title: string,
+navTitle: ?string,
|};
@ -18,13 +20,19 @@ type RouteDatum = {|
const routeData /*: $ReadOnlyArray<RouteDatum> */ = [
{
path: "/",
component: () => require("./HomePage").default,
contents: {
type: "PAGE",
component: () => require("./HomePage").default,
},
title: "SourceCred",
navTitle: "Home",
},
{
path: "/explorer",
component: () => require("./credExplorer/App").default,
contents: {
type: "PAGE",
component: () => require("./credExplorer/App").default,
},
title: "SourceCred explorer",
navTitle: "Explorer",
},
@ -40,6 +48,7 @@ function resolveRouteFromPath(path /*: string */) /*: ?RouteDatum */ {
};
return routeData.filter(matches)[0] || null;
}
exports.resolveRouteFromPath = resolveRouteFromPath;
function resolveTitleFromPath(path /*: string */) /*: string */ {
const route = resolveRouteFromPath(path);

View File

@ -5,47 +5,89 @@ import React from "react";
import ReactDOMServer from "react-dom/server";
import {match, RouterContext} from "react-router";
import Page from "./Page";
import ExternalRedirect from "./ExternalRedirect";
import {createRoutes} from "./createRoutes";
import {resolveTitleFromPath} from "./routeData";
import {resolveRouteFromPath, resolveTitleFromPath} from "./routeData";
import dedent from "../util/dedent";
export default function render(
locals: {+path: string, +assets: {[string]: string}},
callback: (error: ?mixed, result?: string) => void
): void {
const bundlePath = locals.assets["main"];
const url = locals.path;
const routes = createRoutes();
match({routes, location: url}, (error, redirectLocation, renderProps) => {
if (error) {
callback(error);
} else if (renderProps) {
const component = <RouterContext {...renderProps} />;
const {html, css} = StyleSheetServer.renderStatic(() =>
ReactDOMServer.renderToString(component)
);
const page = dedent`\
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="shortcut icon" href="/favicon.ico" />
<title>${resolveTitleFromPath(url)}</title>
<style>${require("./index.css")}</style>
<style data-aphrodite>${css.content}</style>
</head>
<body style="overflow-y:scroll">
<div id="root">${html}</div>
<script src="${bundlePath}"></script>
</body>
</html>
`;
callback(null, page);
const path = locals.path;
{
const route = resolveRouteFromPath(path);
if (route && route.contents.type === "EXTERNAL_REDIRECT") {
return renderRedirect(route.contents.redirectTo);
} else {
// This shouldn't happen because we should only be visiting
// the right routes.
throw new Error(`unexpected 404 from ${url}`);
return renderStandardRoute();
}
});
}
function renderStandardRoute() {
const bundlePath = locals.assets["main"];
const routes = createRoutes();
match({routes, location: path}, (error, redirectLocation, renderProps) => {
if (error) {
callback(error);
} else if (renderProps) {
const component = <RouterContext {...renderProps} />;
const {html, css} = StyleSheetServer.renderStatic(() =>
ReactDOMServer.renderToString(component)
);
const page = dedent`\
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="shortcut icon" href="/favicon.ico" />
<title>${resolveTitleFromPath(path)}</title>
<style>${require("./index.css")}</style>
<style data-aphrodite>${css.content}</style>
</head>
<body style="overflow-y:scroll">
<div id="root">${html}</div>
<script src="${bundlePath}"></script>
</body>
</html>
`;
callback(null, page);
} else {
// This shouldn't happen because we should only be visiting
// the right routes.
throw new Error(`unexpected 404 from ${path}`);
}
});
}
function renderRedirect(redirectTo: string) {
const component = (
<Page>
<ExternalRedirect redirectTo={redirectTo} />
</Page>
);
const {html, css} = StyleSheetServer.renderStatic(() =>
ReactDOMServer.renderToStaticMarkup(component)
);
const page = dedent`\
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta http-equiv="refresh" content="0;url=${redirectTo}" />
<link rel="shortcut icon" href="/favicon.ico" />
<title>${resolveTitleFromPath(path)}</title>
<style>${require("./index.css")}</style>
<style data-aphrodite>${css.content}</style>
</head>
<body style="overflow-y:scroll">
<div id="root">${html}</div>
</body>
</html>
`;
callback(null, page);
}
}