Remove frontend2 routing and radically simplify

Per discussion with @hammadj, @topocount, and @wchargin, we are planning
to have the frontend2 system use react-admin at the top level. Per
investigation by @topocount, react-admin conflicts with the older
version of react-router that we use.

As such, this commit wildly simplifies the homepage2 system so we no
longer have any routing, and instead we just statically render the
index.html file. We also removed the `Assets` type, not because we are
sure we don't need it, but because we didn't want to debug it while we
were all pairing. @wchargin offered to fix it up later.

Test plan:
- run `yarn start2 --instance=PATH` and observe that the "Under
Construction" message displays, along with console messages showing that
data loaded successfully.
- run `yarn build2` and copy files from `build2` into the root of a cli2
instance. Run an http server in that instance, and observe that the
frontend displays properly per instructions above.

Paired with: @wchargin
Paired with: @hammadj
Paired with: @topocount
This commit is contained in:
Dandelion Mané 2020-06-17 19:43:34 -07:00
parent d90521e7da
commit 7ec34edd0d
10 changed files with 26 additions and 397 deletions

View File

@ -235,9 +235,7 @@ async function plugins(mode /*: "development" | "production" */) {
const basePlugins = [
new StaticSiteGeneratorPlugin({
entry: "ssr",
paths: require("../src/homepage2/routeData")
.makeRouteData()
.map(({path}) => path),
paths: ["/"],
locals: {},
}),
new CopyPlugin([{from: paths.favicon, to: "favicon.png"}]),

View File

@ -1,28 +1,24 @@
// @flow
import React from "react";
import {Router} from "react-router";
import type {History /* actually `any` */} from "history";
import {createRoutes} from "./createRoutes";
import {type RouteData, resolveTitleFromPath} from "./routeData";
async function loadAndReport(path) {
const response = await fetch(path);
if (!response.ok) {
console.error(path, response);
}
const json = await response.json();
console.log(path, json);
}
export default class App extends React.Component<{||}> {
async componentDidMount() {
loadAndReport("sourcecred.json");
loadAndReport("output/credResult.json");
loadAndReport("config/sourcecred/discourse/config.json");
}
export default class App extends React.Component<{|
+routeData: RouteData,
+history: History,
|}> {
render() {
const {routeData, history} = this.props;
return (
<Router
history={history}
routes={createRoutes(routeData)}
onUpdate={function () {
const router = this;
const path: string = router.state.location.pathname;
document.title = resolveTitleFromPath(routeData, path);
}}
/>
);
return <h1>Under Construction</h1>;
}
}

View File

@ -1,28 +0,0 @@
// @flow
import React from "react";
import Link from "../webutil/Link";
export default class ExternalRedirect extends React.Component<{|
+redirectTo: string,
|}> {
render() {
return (
<div style={{maxWidth: 900, margin: "0 auto"}}>
<h1>Redirecting</h1>
<p>
Redirecting to:{" "}
<Link href={this.props.redirectTo}>{this.props.redirectTo}</Link>
</p>
</div>
);
}
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

@ -1,43 +0,0 @@
// @flow
import React from "react";
import type {Assets} from "../webutil/assets";
import {StyleSheet, css} from "aphrodite/no-important";
async function loadAndReport(assets, path) {
const url = assets.resolve(path);
const response = await fetch(url);
if (!response.ok) {
console.error(path, response);
}
const json = await response.json();
console.log(path, json);
}
export default class HomePage extends React.Component<{|+assets: Assets|}> {
async componentDidMount() {
loadAndReport(this.props.assets, "sourcecred.json");
loadAndReport(this.props.assets, "output/credResult.json");
loadAndReport(this.props.assets, "config/sourcecred/discourse/config.json");
}
render() {
return (
<div className={css(styles.container)}>
<h1>Under Construction</h1>
</div>
);
}
}
const styles = StyleSheet.create({
container: {
maxWidth: 900,
margin: "0 auto",
marginBottom: 200,
padding: "0 10px",
lineHeight: 1.5,
fontSize: 20,
},
});

View File

@ -1,53 +0,0 @@
// @flow
import React, {type Node} from "react";
import {StyleSheet, css} from "aphrodite/no-important";
import type {Assets} from "../webutil/assets";
import type {RouteData} from "./routeData";
import {VERSION_SHORT, VERSION_FULL} from "../core/version";
export default class Page extends React.Component<{|
+assets: Assets,
+routeData: RouteData,
+children: Node,
|}> {
render() {
return (
<React.Fragment>
<div className={css(style.nonFooter)}>
<main>{this.props.children}</main>
</div>
<footer className={css(style.footer)}>
<div className={css(style.footerWrapper)}>
<span className={css(style.footerText)}>
({VERSION_FULL}) <strong>{VERSION_SHORT}</strong>
</span>
</div>
</footer>
</React.Fragment>
);
}
}
const footerHeight = 30;
const style = StyleSheet.create({
footer: {
color: "#666",
height: footerHeight,
fontSize: 14,
position: "relative",
},
footerWrapper: {
textAlign: "right",
position: "absolute",
bottom: 5,
width: "100%",
},
footerText: {
marginRight: 5,
},
nonFooter: {
minHeight: `calc(100vh - ${footerHeight}px)`,
},
});

View File

@ -1,53 +0,0 @@
// @flow
import React from "react";
import {IndexRoute, Route} from "react-router";
import withAssets from "../webutil/withAssets";
import ExternalRedirect from "./ExternalRedirect";
import Page from "./Page";
import type {RouteData} from "./routeData";
export function createRoutes(routeData: RouteData) {
const PageWithAssets = withAssets(Page);
const PageWithRoutes = (props) => (
<PageWithAssets routeData={routeData} {...props} />
);
return (
<Route path="/" component={PageWithRoutes}>
{routeData.map(({path, contents}) => {
switch (contents.type) {
case "PAGE":
if (path === "/") {
return (
<IndexRoute
key={path}
component={withAssets(contents.component())}
/>
);
} else {
return (
<Route
key={path}
path={path}
component={withAssets(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

@ -1,31 +1,15 @@
// @flow
import React from "react";
import ReactDOM from "react-dom";
import createBrowserHistory from "history/lib/createBrowserHistory";
import normalize from "../util/pathNormalize";
import createRelativeHistory from "../webutil/createRelativeHistory";
import App from "./App";
import {makeRouteData} from "./routeData";
const target = document.getElementById("root");
if (target == null) {
throw new Error("Unable to find root element!");
}
let initialRoot: string = target.dataset.initialRoot;
if (initialRoot == null) {
console.error(
`Initial root unset (${initialRoot}): this should not happen! ` +
'Falling back to ".".'
);
initialRoot = ".";
}
const basename = normalize(`${window.location.pathname}/${initialRoot}/`);
const history = createRelativeHistory(createBrowserHistory(), basename);
const routeData = makeRouteData();
ReactDOM.hydrate(<App routeData={routeData} history={history} />, target);
ReactDOM.hydrate(<App />, target);
// In Chrome, relative favicon URLs are recomputed at every pushState,
// although other assets (like the `src` of an `img`) are not. We don't

View File

@ -1,66 +0,0 @@
// @flow
// NOTE: This module must be written in vanilla ECMAScript that can be
// run by Node without a preprocessor. That means that we use `exports`
// and `require` instead of ECMAScript module keywords, we lazy-load all
// dependent modules, and we use the Flow comment syntax instead of the
// inline syntax.
/*::
import type {Assets} from "../webutil/assets";
type RouteDatum = {|
+path: string,
+contents:
| {|
+type: "PAGE",
+component: () => React$ComponentType<{|+assets: Assets|}>,
|}
| {|
+type: "EXTERNAL_REDIRECT",
+redirectTo: string,
|},
+title: string,
+navTitle: ?string,
|};
export type RouteData = $ReadOnlyArray<RouteDatum>;
*/
function makeRouteData() /*: RouteData */ {
return [
{
path: "/",
contents: {
type: "PAGE",
component: () => require("./HomePage").default,
},
title: "SourceCred",
navTitle: "Home",
},
];
}
exports.makeRouteData = makeRouteData;
function resolveRouteFromPath(
routeData /*: RouteData */,
path /*: string */
) /*: ?RouteDatum */ {
const matches = (candidateRoute) => {
const candidatePath = candidateRoute.path;
const start = path.substring(0, candidatePath.length);
const end = path.substring(candidatePath.length);
return start === candidatePath && (end.length === 0 || end === "/");
};
return routeData.filter(matches)[0] || null;
}
exports.resolveRouteFromPath = resolveRouteFromPath;
function resolveTitleFromPath(
routeData /*: RouteData */,
path /*: string */
) /*: string */ {
const route = resolveRouteFromPath(routeData, path);
const fallback = "SourceCred";
return route ? route.title : fallback;
}
exports.resolveTitleFromPath = resolveTitleFromPath;

View File

@ -1,44 +0,0 @@
// @flow
import {makeRouteData as routeData} from "./routeData";
describe("homepage2/routeData", () => {
/*
* React Router doesn't support relative paths. I'm not sure exactly
* what a path without a leading slash would do; it's asking for
* trouble. If we need them, we can reconsider this test.
*/
it("every path has a leading slash", () => {
for (const route of routeData()) {
if (!route.path.startsWith("/")) {
expect(route.path).toEqual("/" + route.path);
}
}
});
/*
* A route representing a page should have a trailing slash so that
* relative links work in the expected way. For instance, a route
* "/about/team/" may reference "/about/logo.png" via "../logo.png".
* But for the route "/about/team", "../logo.png" refers instead to
* "/logo.png", which is not the intended semantics. Therefore, we
* should consistently either include or omit trailing slashes to
* avoid confusion.
*
* The choice is made for us by the fact that many web servers
* (prominently, GitHub Pages and Python's SimpleHTTPServer) redirect
* "/foo" to "/foo/" when serving "/foo/index.html".
*
* In theory, we might have some file routes like "/about/data.csv"
* that we actually want to appear without a trailing slash. But those
* are outside the scope of our React application, and should be
* handled by a different pipeline (e.g., `copy-webpack-plugin`).
*/
it("every path has a trailing slash", () => {
for (const route of routeData()) {
if (!route.path.endsWith("/")) {
expect(route.path).toEqual(route.path + "/");
}
}
});
});

View File

@ -1,24 +1,12 @@
// @flow
import {StyleSheetServer} from "aphrodite/no-important";
import createMemoryHistory from "history/lib/createMemoryHistory";
import React from "react";
import ReactDOMServer from "react-dom/server";
import {match, RouterContext} from "react-router";
import dedent from "../util/dedent";
import {Assets, rootFromPath} from "../webutil/assets";
import createRelativeHistory from "../webutil/createRelativeHistory";
import ExternalRedirect from "./ExternalRedirect";
import Page from "./Page";
import {createRoutes} from "./createRoutes";
import {
makeRouteData,
resolveRouteFromPath,
resolveTitleFromPath,
} from "./routeData";
const routeData = makeRouteData();
import App from "./App";
export default function render(
locals: {+path: string, +assets: {[string]: string}},
@ -27,63 +15,13 @@ export default function render(
const path = locals.path;
const root = rootFromPath(path);
const assets = new Assets(root);
const history = createRelativeHistory(createMemoryHistory(path), "/");
{
const route = resolveRouteFromPath(routeData, path);
if (route && route.contents.type === "EXTERNAL_REDIRECT") {
return renderRedirect(route.contents.redirectTo);
} else {
return renderStandardRoute();
}
}
return renderStandardRoute();
function renderStandardRoute() {
const bundlePath = locals.assets["main"];
const routes = createRoutes(routeData);
match({history, routes}, (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="${assets.resolve("/favicon.png")}" />
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed" rel="stylesheet">
<title>${resolveTitleFromPath(routeData, path)}</title>
<style>${require("./index.css")}</style>
<style data-aphrodite>${css.content}</style>
</head>
<body style="overflow-y:scroll">
<div id="root" data-initial-root="${root}">${html}</div>
<script src="${assets.resolve(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 routeData={routeData} assets={assets}>
<ExternalRedirect redirectTo={redirectTo} />
</Page>
);
const component = <App />;
const {html, css} = StyleSheetServer.renderStatic(() =>
ReactDOMServer.renderToStaticMarkup(component)
ReactDOMServer.renderToString(component)
);
const page = dedent`\
<!DOCTYPE html>
@ -91,16 +29,16 @@ export default function render(
<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="${assets.resolve("favicon.png")}" />
<link rel="shortcut icon" href="${assets.resolve("/favicon.png")}" />
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed" rel="stylesheet">
<title>${resolveTitleFromPath(routeData, path)}</title>
<title>SourceCred</title>
<style>${require("./index.css")}</style>
<style data-aphrodite>${css.content}</style>
</head>
<body style="overflow-y:scroll">
<div id="root">${html}</div>
<div id="root" data-initial-root="${root}">${html}</div>
<script src="${assets.resolve(bundlePath)}"></script>
</body>
</html>
`;