refactor(@embark/api): catch-all route, fallback pages for Cockpit

Implement a `/*` server-side catch-all route for Cockpit that loads Cockpit's
`index.html`.

This change is necessary with intent to disable offline-first behavior in
production builds of Cockpit. Cockpit's service worker effectively translates
server-side route unavailability into client-side behaviors of
connected-react-router. When the service worker is unregistered the same will
be accomplished via the server-side catch-all route.

Implement fallback pages for when embark is in the monorepo but Cockpit's
Create React App development server isn't yet started or isn't yet responsive.

Implement a fallback page for when the static build of Cockpit is missing. When
embark is in the monorepo, give instructions for building Cockpit. Otherwise,
report that the distribution is broken.

Deprecate the environment variable `EMBARK_DEVELOPMENT` in favor of
`EMBARK_UI_STATIC`. Unless the latter is truthy at runtime, when embark is in
the monorepo the CRA dev server of Cockpit will be accessible at
`localhost:55555` via proxied requests to `localhost:3000`. The deprecation is
not a breaking change as `EMBARK_DEVELOPMENT` / `EMBARK_UI_STATIC` are not
relevant to normal users, but only to developers working on embark itself.

Bump `express-http-proxy` to the latest version.
This commit is contained in:
Michael Bradley, Jr 2019-03-21 10:36:24 -05:00 committed by Iuri Matias
parent e4d1e4ea87
commit 9829e925b4
3 changed files with 172 additions and 25 deletions

View File

@ -96,7 +96,7 @@
"ethereumjs-util": "6.0.0",
"ethereumjs-wallet": "0.6.0",
"express": "4.16.3",
"express-http-proxy": "1.5.0",
"express-http-proxy": "1.5.1",
"express-ws": "4.0.0",
"file-loader": "2.0.0",
"find-up": "2.1.0",
@ -189,7 +189,7 @@
"@types/body-parser": "1.17.0",
"@types/cors": "2.8.4",
"@types/express": "4.16.0",
"@types/express-http-proxy": "1.5.0",
"@types/express-http-proxy": "1.5.1",
"@types/express-ws": "3.0.0",
"@types/find-up": "2.1.0",
"@types/globule": "1.1.3",

View File

@ -1,7 +1,7 @@
import bodyParser from "body-parser";
import cors from "cors";
import { Embark, Plugins } from "embark";
import express, { NextFunction, Request, Response } from "express";
import {Embark, Plugins} from "embark";
import express, {NextFunction, Request, Response} from "express";
import proxy from "express-http-proxy";
import expressWs from "express-ws";
import findUp from "find-up";
@ -11,7 +11,7 @@ import {__} from "i18n";
import * as path from "path";
import * as ws from "ws";
// @ts-ignore
import { embarkPath } from "../../core/fs";
import {embarkPath, existsSync} from "../../core/fs";
type Method = "get" | "post" | "ws" | "delete";
@ -22,8 +22,14 @@ interface CallDescription {
}
export default class Server {
private isLogging: boolean = false;
private _isInsideMonorepo: boolean | null = null;
private _monorepoRootDir: string = "";
private embarkUiBuildDir: string = (
findUp.sync("node_modules/embark-ui/build", {cwd: embarkPath()}) ||
embarkPath("node_modules/embark-ui/build")
);
private expressInstance: expressWs.Instance;
private isLogging: boolean = false;
private server?: http.Server;
constructor(private embark: Embark, private port: number, private hostname: string, private plugins: Plugins) {
@ -38,6 +44,22 @@ export default class Server {
this.isLogging = false;
}
private get isInsideMonorepo() {
if (this._isInsideMonorepo === null) {
this._isInsideMonorepo = existsSync(embarkPath("../../packages/embark")) &&
existsSync(embarkPath("../../lerna.json")) &&
path.resolve(embarkPath("../../packages/embark")) === embarkPath();
}
return this._isInsideMonorepo;
}
private get monorepoRootDir() {
if (!this._monorepoRootDir && this.isInsideMonorepo) {
this._monorepoRootDir = path.resolve(embarkPath("../.."));
}
return this._monorepoRootDir;
}
public start() {
return new Promise<void>((resolve, reject) => {
if (this.server) {
@ -65,9 +87,78 @@ export default class Server {
});
}
private makePage(reloadSeconds: number, body: string) {
return (`
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
${this.isInsideMonorepo ? `
<meta http-equiv="refresh" content="${reloadSeconds}">
` : ""}
<title>Embark API Server</title>
<style type="text/css">
code {
background-color: rgba(220,220,220,.5);
}
${this.isInsideMonorepo ? `
#timer {
background-color: black;
color: yellow;
padding: 0.25em;
}
` : ""}
</style>
</head>
<body>
${body}
${this.isInsideMonorepo ? `
<p>this page will automatically reload
in <span id="timer">${reloadSeconds}</span> seconds</p>
<script>
let timeLeft = ${reloadSeconds};
const span = document.querySelector("#timer");
setInterval(() => {
if (timeLeft >= 1) { timeLeft -= 1; }
span.innerText = \`\${timeLeft}\`;
}, 1000);
</script>
` : ""}
</body>
</html>
`.trim().split("\n").map((str) => str.trim()).filter((str) => str).join("\n"));
}
private makePage404(reloadSeconds: number, envReport: string, inside: string, notice: string) {
return this.makePage(reloadSeconds, `
${envReport}
<p>missing build for package <code>embark-ui</code> ${inside}</p>
${notice}
`);
}
private makePageEConnError(reloadSeconds: number, waitingFor: string) {
return this.makePage(reloadSeconds, `
<p><code>lib/modules/api/server</code> inside the monorepo at
<code>${path.join(this.monorepoRootDir, "packages/embark")}</code> is
waiting for the Create React App development server of package
<code>embark-ui</code> to ${waitingFor} at
<code>localhost:55555</code></p>
${waitingFor === "become available" ? `
<p>please run either:</p>
<p><code>cd ${this.monorepoRootDir} && yarn start</code><br />
or<br />
<code>cd ${path.join(this.monorepoRootDir, "packages/embark-ui")}
&& yarn start</code></p>
<p>to instead use a static build from the monorepo, restart embark with:
<code>EMBARK_UI_STATIC=t embark run</code></p>
` : ""}
`);
}
private initApp() {
const instance = expressWs(express());
instance.app.use((req: Request, res: Response, next: NextFunction) => {
instance.app.use((req: Request, _res, next: NextFunction) => {
if (!this.isLogging) {
return next();
}
@ -91,7 +182,7 @@ export default class Server {
});
if (this.plugins) {
instance.app.get("/embark-api/plugins", (req: Request, res: Response) => {
instance.app.get("/embark-api/plugins", (_req, res: Response) => {
res.send(JSON.stringify(this.plugins.plugins.map((plugin) => ({name: plugin.name}))));
});
@ -101,18 +192,73 @@ export default class Server {
this.embark.events.on("plugins:register:api", (callDescription: CallDescription) => this.registerCallDescription(instance, callDescription));
let ui: express.RequestHandler;
if (process.env.EMBARK_DEVELOPMENT) {
ui = proxy("http://localhost:3000");
if (!this.isInsideMonorepo || process.env.EMBARK_UI_STATIC) {
if (existsSync(path.join(this.embarkUiBuildDir, "index.html"))) {
instance.app.use("/", express.static(this.embarkUiBuildDir));
instance.app.get("/*", (_req, res) => {
res.sendFile(path.join(this.embarkUiBuildDir, "index.html"));
});
} else {
let envReport = "";
let inside = `
in <code>${path.dirname(this.embarkUiBuildDir)}</code>
`;
let notice = `
<p>this distribution of <code>embark-ui</code> appears to be broken</p>
`;
if (this.isInsideMonorepo) {
envReport = `
<p><code>process.env.EMBARK_UI_STATIC ===
${JSON.stringify(process.env.EMBARK_UI_STATIC)}</code></p>
`;
inside = `
inside the monorepo at <code>
${path.join(this.monorepoRootDir, "packages/embark-ui")}</code>
`;
notice = `
<p>to build <code>embark-ui</code> please run either:</p>
<p><code>cd ${this.monorepoRootDir} && yarn build</code><br />
or<br />
<code>cd ${path.join(this.monorepoRootDir, "packages/embark-ui")}
&& yarn build</code></p>
<p>restart <code>embark run</code> after building
<code>embark-ui</code></p>
<p>to instead use a live development build from the monorepo, unset
the environment variable <code>EMBARK_UI_STATIC</code> and restart
embark</p>
`;
}
const page404 = this.makePage404(3, envReport, inside, notice);
const missingBuildHandler = (_req: Request, res: Response) => {
res.status(404).send(page404);
};
instance.app.get("/", missingBuildHandler);
instance.app.get("/*", missingBuildHandler);
}
} else {
ui = express.static(
findUp.sync("node_modules/embark-ui/build", {cwd: embarkPath()}) ||
embarkPath("node_modules/embark-ui/build"),
);
const page503 = this.makePageEConnError(3, "become available");
const page504 = this.makePageEConnError(3, "become responsive");
instance.app.use("/", proxy("http://localhost:3000", {
// @ts-ignore
proxyErrorHandler: (err, res, next) => {
switch (err && err.code) {
case "ECONNREFUSED": {
return res.status(503).send(page503);
}
case "ECONNRESET": {
if (err.message === "socket hang up") {
return res.status(504).send(page504);
}
}
default: {
next(err);
}
}
},
timeout: 1000,
}));
}
instance.app.use("/", ui);
return instance;
}

View File

@ -2716,12 +2716,13 @@
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
integrity sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==
"@types/express-http-proxy@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@types/express-http-proxy/-/express-http-proxy-1.5.0.tgz#0543348e5845c4c87f6e17a834f4962a75ea4579"
integrity sha512-gPo4mt1//QMitFvyw0AXrjtmp1xrGQtxLNJdRcXD4QkTv4RvEFXU01/8lYlFTsNr8AL/10QyRdrBeC9Ow9WuSA==
"@types/express-http-proxy@1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@types/express-http-proxy/-/express-http-proxy-1.5.1.tgz#0184017b1cfc8ab2a4954d35f90c9b4cc3d7ffcc"
integrity sha512-9SOGqwVzbudT5nzF4TjKOu0cWE0HRaTVVivwxUxYMN/7mas6Wt/W5pz53dZIs7Y0fZBjAI3RTDDr+dXtXrv+hA==
dependencies:
"@types/express" "*"
"@types/express-serve-static-core" "*"
"@types/express-serve-static-core@*":
version "4.16.0"
@ -8542,10 +8543,10 @@ expect@^23.6.0:
jest-message-util "^23.4.0"
jest-regex-util "^23.3.0"
express-http-proxy@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/express-http-proxy/-/express-http-proxy-1.5.0.tgz#27aca3898e2f0cb9aff2e1d00c93ec27014254a2"
integrity sha512-rYXjOj+ldSDZdmCxRDX/7o6Oxtz45sS9l4QTsvqm+ZFxqI5xTA4usMMP4FBrrKTpDPuQkI2YVda+0LvkJhPu7A==
express-http-proxy@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/express-http-proxy/-/express-http-proxy-1.5.1.tgz#cbf45695c759693c9c5f946117462d25b57e77a8"
integrity sha512-k1RdysZWZ8wdPnsLa4iyrrYyUFih/sYKkn6WfkU/q5A8eUdh3l+oXhrRuQmEYEsZmiexVvpiOCkogl03jYfcbg==
dependencies:
debug "^3.0.1"
es6-promise "^4.1.1"