refactor(@embark/api): in dev use cockpit redirect instead of proxy

Embark API server's development proxy from port 55555 to 3000 was attempting to
inappropriately forward an `/embark-api/` endpoint for the blockchain process
logs to Create React App's development server. Why it was only happening for
the one endpoint is not known but probably has to do with timing around
registration of the API server's express routes.

The problem can be fixed with a one-line `filter:` function in the options for
`express-http-proxy`. However, it was realized that to fix an unrelated
problem, whereby the proxy doesn't forward websockets to CRA such that hot
reload doesn't work when accessing `embark-ui` in development on port 55555, a
switch to `http-proxy-middleware` would be required. That was quickly
attempted (easy switch) but there are outstanding [difficulties][bug] with
`webpack-dev-server` and `node-http-proxy` that cause CRA to crash.

Switch strategies and refactor the API module to serve a page on port 55555 (in
development only) that alerts the developer `embark-ui` should be accessed on
port 3000. The page redirects (client-side) after 10 seconds, with URL query
params and/or hash preserved. A future version could instead do client-side
polling of port 3000 with `fetch` and then redirect only once it's
available. The reason for not redirecting immediately is that the intermediate
page makes it more obvious what needs to be done, e.g. CRA dev server may need
to be started with `yarn start`.

[bug]: https://github.com/webpack/webpack-dev-server/issues/1642
This commit is contained in:
Michael Bradley, Jr 2019-04-15 18:58:25 -05:00 committed by Iuri Matias
parent 3988fb4c8a
commit a39c2c82d7
3 changed files with 74 additions and 70 deletions

View File

@ -191,7 +191,6 @@
"@types/body-parser": "1.17.0", "@types/body-parser": "1.17.0",
"@types/cors": "2.8.4", "@types/cors": "2.8.4",
"@types/express": "4.16.0", "@types/express": "4.16.0",
"@types/express-http-proxy": "1.5.1",
"@types/express-ws": "3.0.0", "@types/express-ws": "3.0.0",
"@types/find-up": "2.1.0", "@types/find-up": "2.1.0",
"@types/globule": "1.1.3", "@types/globule": "1.1.3",

View File

@ -2,7 +2,6 @@ import bodyParser from "body-parser";
import cors from "cors"; import cors from "cors";
import {Embark, Plugins} from "embark"; import {Embark, Plugins} from "embark";
import express, {NextFunction, Request, Response} from "express"; import express, {NextFunction, Request, Response} from "express";
import proxy from "express-http-proxy";
import expressWs from "express-ws"; import expressWs from "express-ws";
import findUp from "find-up"; import findUp from "find-up";
import helmet from "helmet"; import helmet from "helmet";
@ -87,15 +86,12 @@ export default class Server {
}); });
} }
private makePage(reloadSeconds: number, body: string) { private makePage(body: string) {
return (` return (`
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
${this.isInsideMonorepo ? `
<meta http-equiv="refresh" content="${reloadSeconds}">
` : ""}
<title>Embark API Server</title> <title>Embark API Server</title>
<style type="text/css"> <style type="text/css">
code { code {
@ -112,47 +108,78 @@ export default class Server {
</head> </head>
<body> <body>
${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> </body>
</html> </html>
`.trim().split("\n").map((str) => str.trim()).filter((str) => str).join("\n")); `.trim().split("\n").map((str) => str.trim()).filter((str) => str).join("\n"));
} }
private makePage404(reloadSeconds: number, envReport: string, inside: string, notice: string) { private makePage404(reloadSeconds: number, envReport: string, inside: string, notice: string) {
return this.makePage(reloadSeconds, ` return this.makePage(`
${envReport} ${envReport}
<p>missing build for package <code>embark-ui</code> ${inside}</p> <p>missing build for package <code>embark-ui</code> ${inside}</p>
${notice} ${notice}
${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");
const timer = window.setInterval(() => {
if (timeLeft >= 1) {
timeLeft -= 1;
span.innerText = \`\${timeLeft}\`;
}
if (!timeLeft) {
window.clearInterval(timer);
window.location.reload(true);
}
}, 1000);
</script>
` : ""}
`); `);
} }
private makePageEConnError(reloadSeconds: number, waitingFor: string) { private makePage503(redirectSeconds: number) {
return this.makePage(reloadSeconds, ` return this.makePage(`
<p><code>lib/modules/api/server</code> inside the monorepo at <p><code>lib/modules/api/server</code> is inside the monorepo at
<code>${path.join(this.monorepoRootDir, "packages/embark")}</code> is <code>${path.join(this.monorepoRootDir, "packages/embark")}</code></p>
waiting for the Create React App development server of package <p>to access <code>embark-ui</code> in development use port
<code>embark-ui</code> to ${waitingFor} at <code>3000</code></p>
<code>localhost:55555</code></p> <p>if you haven't already, please run either:</p>
${waitingFor === "become available" ? `
<p>please run either:</p>
<p><code>cd ${this.monorepoRootDir} && yarn start</code><br /> <p><code>cd ${this.monorepoRootDir} && yarn start</code><br />
or<br /> or<br />
<code>cd ${path.join(this.monorepoRootDir, "packages/embark-ui")} <code>cd ${path.join(this.monorepoRootDir, "packages/embark-ui")} &&
&& yarn start</code></p> yarn start</code></p>
<p>to instead use a static build from the monorepo, restart embark with: <p>to instead use a static build from the monorepo, restart embark with:
<code>EMBARK_UI_STATIC=t embark run</code></p> <code>EMBARK_UI_STATIC=t embark run</code></p>
` : ""} <p>this page will automatically redirect to <a id="redirect" href=""></a>
in <span id="timer">${redirectSeconds}</span> seconds</p>
<script>
window.embarkApiRedirect = window.location.href.replace(
\`http://\${window.location.hostname}:55555\`,
\`http://\${window.location.hostname}:3000\`
);
document.querySelector("#redirect").href = window.embarkApiRedirect;
let displayLink = window.embarkApiRedirect.slice(7);
if (displayLink.endsWith(\`\${window.location.hostname}:3000/\`)) {
displayLink = displayLink.slice(0, -1);
}
document.querySelector("#redirect").innerText = displayLink;
</script>
<script>
let timeLeft = ${redirectSeconds};
const span = document.querySelector("#timer");
const timer = window.setInterval(() => {
if (timeLeft >= 1) {
timeLeft -= 1;
span.innerText = \`\${timeLeft}\`;
}
if (!timeLeft) {
window.clearInterval(timer);
window.location.href = window.embarkApiRedirect;
}
}, 1000);
</script>
`); `);
} }
@ -195,7 +222,7 @@ export default class Server {
if (!this.isInsideMonorepo || process.env.EMBARK_UI_STATIC) { if (!this.isInsideMonorepo || process.env.EMBARK_UI_STATIC) {
if (existsSync(path.join(this.embarkUiBuildDir, "index.html"))) { if (existsSync(path.join(this.embarkUiBuildDir, "index.html"))) {
instance.app.use("/", express.static(this.embarkUiBuildDir)); instance.app.use("/", express.static(this.embarkUiBuildDir));
instance.app.get("/*", (_req, res) => { instance.app.get(/^\/(?!embark-api).*$/, (_req, res) => {
res.sendFile(path.join(this.embarkUiBuildDir, "index.html")); res.sendFile(path.join(this.embarkUiBuildDir, "index.html"));
}); });
} else { } else {
@ -204,7 +231,9 @@ export default class Server {
in <code>${path.dirname(this.embarkUiBuildDir)}</code> in <code>${path.dirname(this.embarkUiBuildDir)}</code>
`; `;
let notice = ` let notice = `
<p>this distribution of <code>embark-ui</code> appears to be broken</p> <p>this distribution of <code>embark-ui</code> appears to be broken,
please <a href="https://github.com/embark-framework/embark/issues">
file an issue</a></p>
`; `;
if (this.isInsideMonorepo) { if (this.isInsideMonorepo) {
envReport = ` envReport = `
@ -223,40 +252,24 @@ export default class Server {
&& yarn build</code></p> && yarn build</code></p>
<p>restart <code>embark run</code> after building <p>restart <code>embark run</code> after building
<code>embark-ui</code></p> <code>embark-ui</code></p>
<p>to instead use a live development build from the monorepo, unset <p>to instead use a live development build from the monorepo: unset
the environment variable <code>EMBARK_UI_STATIC</code> and restart the environment variable <code>EMBARK_UI_STATIC</code>, restart
embark</p> embark, and visit
<a href="http://localhost:3000">http://localhost:3000</a></p>
`; `;
} }
const page404 = this.makePage404(3, envReport, inside, notice); const page404 = this.makePage404(10, envReport, inside, notice);
const missingBuildHandler = (_req: Request, res: Response) => { const missingBuildHandler = (_req: Request, res: Response) => {
res.status(404).send(page404); res.status(404).send(page404);
}; };
instance.app.get("/", missingBuildHandler); instance.app.get(/^\/(?!embark-api).*$/, missingBuildHandler);
instance.app.get("/*", missingBuildHandler);
} }
} else { } else {
const page503 = this.makePageEConnError(3, "become available"); const page503 = this.makePage503(10);
const page504 = this.makePageEConnError(3, "become responsive"); const unavailableBuildHandler = (_req: Request, res: Response) => {
instance.app.use("/", proxy("http://localhost:3000", { res.status(503).send(page503);
// @ts-ignore };
proxyErrorHandler: (err, res, next) => { instance.app.get(/^\/(?!embark-api).*$/, unavailableBuildHandler);
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,
}));
} }
return instance; return instance;

View File

@ -2723,14 +2723,6 @@
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
integrity sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA== integrity sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==
"@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@*": "@types/express-serve-static-core@*":
version "4.16.0" version "4.16.0"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz#fdfe777594ddc1fe8eb8eccce52e261b496e43e7" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz#fdfe777594ddc1fe8eb8eccce52e261b496e43e7"