From f70ecb4953c57e20b9eb9ed149775ef656ff1e24 Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Sat, 2 Mar 2019 20:00:18 -0800 Subject: [PATCH 01/11] Add youch + youch-terminal --- packages/polydev/package.json | 4 +- .../polydev/src/middleware/error/index.js | 37 ++++++------------- .../polydev/src/middleware/router/launcher.js | 2 +- yarn.lock | 28 +++++++++++++- 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/packages/polydev/package.json b/packages/polydev/package.json index 79ede6d..24e5875 100644 --- a/packages/polydev/package.json +++ b/packages/polydev/package.json @@ -21,6 +21,8 @@ "opn": "^5.4.0", "raw-body": "^2.3.3", "uuid": "^3.3.2", - "wait-on": "^3.2.0" + "wait-on": "^3.2.0", + "youch": "^2.0.10", + "youch-terminal": "^1.0.0" } } diff --git a/packages/polydev/src/middleware/error/index.js b/packages/polydev/src/middleware/error/index.js index 08377c9..081e35c 100644 --- a/packages/polydev/src/middleware/error/index.js +++ b/packages/polydev/src/middleware/error/index.js @@ -1,32 +1,17 @@ +const Youch = require("youch") +const forTerminal = require("youch-terminal") + module.exports = function errorHandler(error, req, res, next) { const { status = "", statusCode = 500 } = error - res.status(statusCode).send(` - - - - -
- -
-
-

- ${statusCode} ${status} -

+ const youch = new Youch(error, req) -
${error.message}
-
+ youch.toHTML().then((html) => { + res.status(statusCode).send(html) + }) - ${ - error.stack - ? ` -
-
${error.stack}
-
- ` - : "" - } -
- - `) + youch + .toJSON() + .then(forTerminal) + .then(console.log) } diff --git a/packages/polydev/src/middleware/router/launcher.js b/packages/polydev/src/middleware/router/launcher.js index bf52182..cefde79 100644 --- a/packages/polydev/src/middleware/router/launcher.js +++ b/packages/polydev/src/middleware/router/launcher.js @@ -70,7 +70,7 @@ async function startHandler() { app, route, // Make sure we always evaluate at run-time for the latest HMR'd handler - (req, res, next) => { + function handleRoute(req, res, next) { const handled = handler(req, res, next) // Automatically bubble up async errors diff --git a/yarn.lock b/yarn.lock index b249c25..c7d865e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2022,7 +2022,7 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.3.1: +cookie@0.3.1, cookie@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= @@ -4016,6 +4016,11 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== +mustache@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-3.0.1.tgz#873855f23aa8a95b150fb96d9836edbc5a1d248a" + integrity sha512-jFI/4UVRsRYdUbuDTKT7KzfOp7FiD5WzYmmwNwXyUVypC0xjoTL78Fqc0jHUPIvvGD+6DQSPHIt1NE7D1ArsqA== + nan@^2.9.2: version "2.12.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" @@ -5339,6 +5344,11 @@ ssri@^6.0.1: dependencies: figgy-pudding "^3.5.1" +stack-trace@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + stackframe@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b" @@ -6125,6 +6135,22 @@ yn@^2.0.0: resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" integrity sha1-5a2ryKz0CPY4X8dklWhMiOavaJo= +youch-terminal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/youch-terminal/-/youch-terminal-1.0.0.tgz#03e2096ee360ef915816e62ea9ec94a6ff094d9e" + integrity sha512-rOVcJi5juKSUI3/mwAKOP+gjrUWUgb5AUTe2LZlfLBgLdfgnYtBKCrWc02GgRozDf/i6uXBu9/y3Vf6db+7k1A== + dependencies: + chalk "^2.3.0" + +youch@^2.0.10: + version "2.0.10" + resolved "https://registry.yarnpkg.com/youch/-/youch-2.0.10.tgz#e0f6312b12304fd330a0c4a0e0925b0123f7d495" + integrity sha512-qPLQW2TuwlcK9sm5i1Gbb9ezRZRZyzr6NsY5cqxsbh+2iEyKPxLlz0OSAc+pQ7mv1pYZLri1MXynggP6R2FcNQ== + dependencies: + cookie "^0.3.1" + mustache "^3.0.0" + stack-trace "0.0.10" + zen-observable-ts@^0.8.13: version "0.8.13" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.13.tgz#ae1fd77c84ef95510188b1f8bca579d7a5448fc2" From 59eaf0eb85452885afaa1381c859368b27ca8d68 Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Sat, 2 Mar 2019 20:24:19 -0800 Subject: [PATCH 02/11] Prettier error page --- .../polydev/src/middleware/error/index.js | 21 ++++++++++++ packages/polydev/src/public/styles.css | 33 ++++++++++++++----- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/packages/polydev/src/middleware/error/index.js b/packages/polydev/src/middleware/error/index.js index 081e35c..1846379 100644 --- a/packages/polydev/src/middleware/error/index.js +++ b/packages/polydev/src/middleware/error/index.js @@ -6,6 +6,27 @@ module.exports = function errorHandler(error, req, res, next) { const youch = new Youch(error, req) + youch.addLink((error) => { + return ` + +
+ ` + }) + + youch.addLink(({ message }) => { + const url = `https://google.com/search?q=${encodeURIComponent(message)}` + + return `` + }) + + youch.addLink(({ message }) => { + const url = `https://stackoverflow.com/search?q=${encodeURIComponent( + message + )}` + + return `` + }) + youch.toHTML().then((html) => { res.status(statusCode).send(html) }) diff --git a/packages/polydev/src/public/styles.css b/packages/polydev/src/public/styles.css index 1f184d6..6e10427 100644 --- a/packages/polydev/src/public/styles.css +++ b/packages/polydev/src/public/styles.css @@ -36,10 +36,6 @@ justify-content: center; } - body.error { - background: red; - } - form { margin: 0; } @@ -56,10 +52,6 @@ z-index: -1; } - .error #splash { - opacity: 0.5; - } - h1, h2, h3, @@ -69,7 +61,11 @@ font-weight: 500; } - section { + h2.error-message { + text-shadow: 0 1px 0px white + } + + section:not([class]) { background: white; border-radius: 3px; box-shadow: 0 2vw 4vw 0 rgba(0, 0, 0, 0.11), 0 2vw 4vw 0 rgba(0, 0, 0, 0.08); @@ -78,6 +74,25 @@ overflow: auto; } + section.error-page .fab { + color: #455275; + background: rgb(250, 250, 250); + border: 1px solid white; + border-radius: 100em; + box-shadow: 0 0.1rem 0.3rem rgba(0, 0, 0, 0.2); + margin-right: 0.5rem; + padding: 0.5rem 1rem; + } + + section.error-stack { + background: rgba(100%, 100%, 100%, 0.5) + } + + section.request-details { + background: white; + box-shadow: 0 5em 10em black + } + section header, section main, section footer { From 5f9b71f9a44519543e9acc70425cdabedcf3042f Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Sat, 2 Mar 2019 20:46:26 -0800 Subject: [PATCH 03/11] Fix bug where sync handlers don't return an object --- packages/polydev/src/middleware/router/handle.development.js | 2 +- packages/polydev/src/middleware/router/handle.production.js | 2 +- packages/polydev/src/middleware/router/launcher.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/polydev/src/middleware/router/handle.development.js b/packages/polydev/src/middleware/router/handle.development.js index 65f76d6..d7ec582 100644 --- a/packages/polydev/src/middleware/router/handle.development.js +++ b/packages/polydev/src/middleware/router/handle.development.js @@ -110,7 +110,7 @@ module.exports = function handle(router, file, routes) { const handled = handler(req, res, next) // Automatically bubble up async errors - if (handled.catch) { + if (handled && handled.catch) { handled.catch(next) } } diff --git a/packages/polydev/src/middleware/router/handle.production.js b/packages/polydev/src/middleware/router/handle.production.js index 56fa82e..5e3bc87 100644 --- a/packages/polydev/src/middleware/router/handle.production.js +++ b/packages/polydev/src/middleware/router/handle.production.js @@ -26,7 +26,7 @@ module.exports = async function handle(router, file, routes) { const handled = handler(req, res, next) // Automatically bubble up async errors - if (handled.catch) { + if (handled && handled.catch) { handled.catch(next) } } diff --git a/packages/polydev/src/middleware/router/launcher.js b/packages/polydev/src/middleware/router/launcher.js index cefde79..0320cdc 100644 --- a/packages/polydev/src/middleware/router/launcher.js +++ b/packages/polydev/src/middleware/router/launcher.js @@ -74,7 +74,7 @@ async function startHandler() { const handled = handler(req, res, next) // Automatically bubble up async errors - if (handled.catch) { + if (handled && handled.catch) { handled.catch(next) } } From bb8fdfe7010621c0dcdf08dacb3867940765043d Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Sat, 2 Mar 2019 20:47:58 -0800 Subject: [PATCH 04/11] Add missing-module example --- routes/index.js | 3 +++ routes/missing-module/index.js | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 routes/missing-module/index.js diff --git a/routes/index.js b/routes/index.js index 18493a2..8a4586b 100644 --- a/routes/index.js +++ b/routes/index.js @@ -37,6 +37,9 @@ module.exports = (req, res) => {
  • Logo
  • +
  • + Missing Module +
  • Next.js
  • diff --git a/routes/missing-module/index.js b/routes/missing-module/index.js new file mode 100644 index 0000000..25c754c --- /dev/null +++ b/routes/missing-module/index.js @@ -0,0 +1,22 @@ +const humanize = require("humanize") + +module.exports = (req, res) => { + res.send(` + + + + + + +
    + +
    +
    +

    + 👋 Howdy from polydev +

    +
    +
    + + `) +} From ac04822ec0e9c9e788a03377512d54a5a649fc96 Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Sun, 3 Mar 2019 19:52:16 -0800 Subject: [PATCH 05/11] Install missing modules --- packages/polydev/package.json | 2 +- packages/polydev/src/index.js | 11 ++++ .../polydev/src/middleware/error/index.js | 35 ++++++++++++ .../polydev/src/middleware/notFound/index.js | 5 -- .../middleware/router/handle.development.js | 3 +- .../polydev/src/middleware/router/launcher.js | 10 ++++ packages/polydev/src/public/styles.css | 46 +++++++++++++++- .../routes/_polydev/install-module/index.js | 54 +++++++++++++++++++ yarn.lock | 9 +++- 9 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 packages/polydev/src/routes/_polydev/install-module/index.js diff --git a/packages/polydev/package.json b/packages/polydev/package.json index 24e5875..7d5b44f 100644 --- a/packages/polydev/package.json +++ b/packages/polydev/package.json @@ -11,7 +11,6 @@ "src" ], "dependencies": { - "body-parser": "^1.18.3", "chokidar": "^2.0.4", "debug": "^4.1.1", "express": "^4.16.4", @@ -20,6 +19,7 @@ "hot-replacement-module": "https://github.com/ericclemmons/hot-module-replacement.git", "opn": "^5.4.0", "raw-body": "^2.3.3", + "stream-ansi2html": "^1.1.0", "uuid": "^3.3.2", "wait-on": "^3.2.0", "youch": "^2.0.10", diff --git a/packages/polydev/src/index.js b/packages/polydev/src/index.js index d95a471..32d216b 100644 --- a/packages/polydev/src/index.js +++ b/packages/polydev/src/index.js @@ -5,16 +5,27 @@ const middleware = require("./middleware") const { NODE_ENV = "development" } = process.env +const verify = (req, res, buffer, encoding = "utf8") => { + if (buffer && buffer.length) { + req.rawBody = buffer.toString(encoding) + } +} + module.exports.polydev = (options = {}) => { const { assets = "public", routes = "routes" } = options const app = express() + // req.body is needed + app.use(express.urlencoded({ extended: true, verify })) + app.use(express.json({ verify })) + app.use(middleware.assets(assets)) app.use(middleware.router(routes)) // TODO Merge 404 & errors together if (NODE_ENV === "development") { app.use("/_polydev", middleware.assets(path.resolve(__dirname, "./public"))) + app.use(middleware.router(path.resolve(__dirname, "./routes"))) app.use(middleware.notFound) app.use(middleware.error) } diff --git a/packages/polydev/src/middleware/error/index.js b/packages/polydev/src/middleware/error/index.js index 1846379..2d10fc9 100644 --- a/packages/polydev/src/middleware/error/index.js +++ b/packages/polydev/src/middleware/error/index.js @@ -1,6 +1,9 @@ +const generateId = require("uuid/v1") const Youch = require("youch") const forTerminal = require("youch-terminal") +const nonce = generateId() + module.exports = function errorHandler(error, req, res, next) { const { status = "", statusCode = 500 } = error @@ -13,6 +16,38 @@ module.exports = function errorHandler(error, req, res, next) { ` }) + if (error.code === "MODULE_NOT_FOUND") { + const [, missing] = error.message.match(/'(.*)'/) + + youch.addLink( + () => ` +
    + + + + +

    + Would you like to install ${missing}? +

    + + + + +
    + + + + +
    + ` + ) + } + youch.addLink(({ message }) => { const url = `https://google.com/search?q=${encodeURIComponent(message)}` diff --git a/packages/polydev/src/middleware/notFound/index.js b/packages/polydev/src/middleware/notFound/index.js index 324c20b..c168f8c 100644 --- a/packages/polydev/src/middleware/notFound/index.js +++ b/packages/polydev/src/middleware/notFound/index.js @@ -1,4 +1,3 @@ -const bodyParser = require("body-parser") const express = require("express") const jetpack = require("fs-jetpack") const opn = require("opn") @@ -9,10 +8,6 @@ const waitOn = require("wait-on") const nonce = generateId() module.exports = express() - // req.body is needed - .use(bodyParser.urlencoded({ extended: false })) - .use(bodyParser.json()) - // This handler only responds to GET/POST, not HEAD/OPTIONS/etc. .use( function onlyGetPost(req, res, next) { diff --git a/packages/polydev/src/middleware/router/handle.development.js b/packages/polydev/src/middleware/router/handle.development.js index d7ec582..e8175ca 100644 --- a/packages/polydev/src/middleware/router/handle.development.js +++ b/packages/polydev/src/middleware/router/handle.development.js @@ -84,8 +84,7 @@ module.exports = function handle(router, file, routes) { } const event = { - // TODO Replace with body-parser - body: (await rawBody(req)).toString("utf8"), + body: req.rawBody, headers: req.headers, host: req.headers.host, method: req.method, diff --git a/packages/polydev/src/middleware/router/launcher.js b/packages/polydev/src/middleware/router/launcher.js index 0320cdc..d383613 100644 --- a/packages/polydev/src/middleware/router/launcher.js +++ b/packages/polydev/src/middleware/router/launcher.js @@ -21,6 +21,12 @@ const [, , handlerPath, routesString] = process.argv // Expected to be JSON.stringify([["GET", "/"]]) const routes = JSON.parse(routesString) +const verify = (req, res, buffer, encoding = "utf8") => { + if (buffer && buffer.length) { + req.rawBody = buffer.toString(encoding) + } +} + // TODO Remove baseUrl unless it's needed in the route async function startHandler() { const getLatestHandler = async () => { @@ -65,6 +71,10 @@ async function startHandler() { if (typeof handler === "function") { const app = express() + // req.body is needed + app.use(express.urlencoded({ extended: true, verify })) + app.use(express.json({ verify })) + routes.forEach(([method, route]) => { app[method.toLowerCase()].call( app, diff --git a/packages/polydev/src/public/styles.css b/packages/polydev/src/public/styles.css index 6e10427..03c3ee7 100644 --- a/packages/polydev/src/public/styles.css +++ b/packages/polydev/src/public/styles.css @@ -74,14 +74,35 @@ overflow: auto; } - section.error-page .fab { - color: #455275; + section.error-page .fab, + section.error-page button { background: rgb(250, 250, 250); border: 1px solid white; border-radius: 100em; box-shadow: 0 0.1rem 0.3rem rgba(0, 0, 0, 0.2); + color: #455275; + cursor: pointer; margin-right: 0.5rem; padding: 0.5rem 1rem; + transition: all 200ms; + } + + section.error-page .fab:hover, + section.error-page button:hover { + background: white; + border-color: #455275; + box-shadow: 0 0.1rem 0.3rem rgba(0, 0, 0, 0.3); + } + + section.error-page form { + background: rgb(250, 250, 250, 0.5); + border-radius: 3px; + margin-bottom: 2rem; + padding: 1rem; + } + + section.error-page form h3 { + margin-bottom: 1rem; } section.error-stack { @@ -117,3 +138,24 @@ padding: 3px 5px; vertical-align: middle; } + + hr { + background: rgba(0, 0, 0, 0.05); + height: 1px; + border: 0; + margin: 1rem 0; + } + + pre { + animation: fadein 2s; + background: #222; + border-radius: 3px; + color: #fff; + font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif; + font-size: 13px; + height: 25ch; + line-height: 20px; + padding: 1rem; + overflow: auto; + width: 80ch; + } diff --git a/packages/polydev/src/routes/_polydev/install-module/index.js b/packages/polydev/src/routes/_polydev/install-module/index.js new file mode 100644 index 0000000..90f2e7b --- /dev/null +++ b/packages/polydev/src/routes/_polydev/install-module/index.js @@ -0,0 +1,54 @@ +const ansi2html = require("stream-ansi2html") +const { spawn } = require("child_process") + +module.exports = (req, res) => { + if (!req.body.module) { + throw new Error(`Missing module not defined`) + } + + res.writeHead(200, { + "Content-Type": "text/html; charset=utf-8", + "Transfer-Encoding": "chunked", + "X-Content-Type-Options": "nosniff" + }) + + res.write(` + + + + + + +
    + +
    +
    +

    + Installing ${req.body.module}… +

    + +
    `)
    +
    +  const args = ["add", req.body.module]
    +
    +  if (req.body.dev) {
    +    args.push("--dev")
    +  }
    +
    +  const child = spawn("yarn", args)
    +
    +  res.write(`$ yarn ${args.join(" ")}\n`)
    +
    +  child.stdout.pipe(ansi2html()).pipe(res)
    +  child.stderr.pipe(ansi2html()).pipe(res)
    +
    +  child.on("close", (code, signal) => {
    +    if (!code) {
    +      res.write(`
    +        
    +      `)
    +    }
    +
    +    res.end()
    +  })
    +}
    diff --git a/yarn.lock b/yarn.lock
    index c7d865e..e4088a2 100644
    --- a/yarn.lock
    +++ b/yarn.lock
    @@ -5384,6 +5384,13 @@ std-env@^1.1.0, std-env@^1.3.1:
       dependencies:
         is-ci "^1.1.0"
     
    +stream-ansi2html@^1.1.0:
    +  version "1.1.0"
    +  resolved "https://registry.yarnpkg.com/stream-ansi2html/-/stream-ansi2html-1.1.0.tgz#9a9620e81313c065e811aae963b23b962924d026"
    +  integrity sha1-mpYg6BMTwGXoEarpY7I7likk0CY=
    +  dependencies:
    +    through2 "^2.0.1"
    +
     stream-browserify@^2.0.1:
       version "2.0.1"
       resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
    @@ -5591,7 +5598,7 @@ terser@^3.8.1:
         source-map "~0.6.1"
         source-map-support "~0.5.6"
     
    -through2@^2.0.0:
    +through2@^2.0.0, through2@^2.0.1:
       version "2.0.5"
       resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
       integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
    
    From 4039216799555afbadd01aa1a9f3c725545f2546 Mon Sep 17 00:00:00 2001
    From: Eric Clemmons 
    Date: Sun, 3 Mar 2019 19:52:31 -0800
    Subject: [PATCH 06/11] Forward errors if we can't require the handler
    
    ---
     packages/polydev/src/middleware/router/launcher.js | 6 +++++-
     1 file changed, 5 insertions(+), 1 deletion(-)
    
    diff --git a/packages/polydev/src/middleware/router/launcher.js b/packages/polydev/src/middleware/router/launcher.js
    index d383613..d60c312 100644
    --- a/packages/polydev/src/middleware/router/launcher.js
    +++ b/packages/polydev/src/middleware/router/launcher.js
    @@ -37,7 +37,11 @@ async function startHandler() {
       }
     
       // Next.js returns a Promise for when the server is ready
    -  let handler = await getLatestHandler()
    +  let handler = await getLatestHandler().catch((error) => {
    +    return function invalidHandler(req, res, next) {
    +      next(error)
    +    }
    +  })
     
       // @ts-ignore
       if (module.hot) {
    
    From 1b47cbd3e2c0b0e09af14f078858fe2b37c80711 Mon Sep 17 00:00:00 2001
    From: Eric Clemmons 
    Date: Sun, 3 Mar 2019 21:58:42 -0800
    Subject: [PATCH 07/11] Fix sending data to HTML
    
    ---
     packages/polydev/package.json                 |  2 +-
     .../routes/_polydev/install-module/index.js   | 14 ++++++++++---
     yarn.lock                                     | 21 ++++++++++++-------
     3 files changed, 25 insertions(+), 12 deletions(-)
    
    diff --git a/packages/polydev/package.json b/packages/polydev/package.json
    index 7d5b44f..be6825e 100644
    --- a/packages/polydev/package.json
    +++ b/packages/polydev/package.json
    @@ -11,6 +11,7 @@
         "src"
       ],
       "dependencies": {
    +    "ansi-to-html": "^0.6.10",
         "chokidar": "^2.0.4",
         "debug": "^4.1.1",
         "express": "^4.16.4",
    @@ -19,7 +20,6 @@
         "hot-replacement-module": "https://github.com/ericclemmons/hot-module-replacement.git",
         "opn": "^5.4.0",
         "raw-body": "^2.3.3",
    -    "stream-ansi2html": "^1.1.0",
         "uuid": "^3.3.2",
         "wait-on": "^3.2.0",
         "youch": "^2.0.10",
    diff --git a/packages/polydev/src/routes/_polydev/install-module/index.js b/packages/polydev/src/routes/_polydev/install-module/index.js
    index 90f2e7b..115c335 100644
    --- a/packages/polydev/src/routes/_polydev/install-module/index.js
    +++ b/packages/polydev/src/routes/_polydev/install-module/index.js
    @@ -1,6 +1,14 @@
    -const ansi2html = require("stream-ansi2html")
    +const Convert = require("ansi-to-html")
     const { spawn } = require("child_process")
     
    +const convert = new Convert({
    +  fg: "#eee",
    +  bg: "#222",
    +  newline: false,
    +  escapeXML: true,
    +  stream: true
    +})
    +
     module.exports = (req, res) => {
       if (!req.body.module) {
         throw new Error(`Missing module not defined`)
    @@ -39,8 +47,8 @@ module.exports = (req, res) => {
     
       res.write(`$ yarn ${args.join(" ")}\n`)
     
    -  child.stdout.pipe(ansi2html()).pipe(res)
    -  child.stderr.pipe(ansi2html()).pipe(res)
    +  child.stderr.on("data", (data) => res.write(convert.toHtml(`${data}`)))
    +  child.stdout.on("data", (data) => res.write(convert.toHtml(`${data}`)))
     
       child.on("close", (code, signal) => {
         if (!code) {
    diff --git a/yarn.lock b/yarn.lock
    index e4088a2..a29aad0 100644
    --- a/yarn.lock
    +++ b/yarn.lock
    @@ -1123,6 +1123,13 @@ ansi-styles@^3.2.1:
       dependencies:
         color-convert "^1.9.0"
     
    +ansi-to-html@^0.6.10:
    +  version "0.6.10"
    +  resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.10.tgz#412114353bac2589a034db7ec5b371b8ba771131"
    +  integrity sha512-znsY3gvsk4CiApWu1yVYF8Nx5Vy0FEe8B0YwyxdbCdErJu5lfKlRHB2twtUjR+dxR4WewTk2OP8XqTmWYnImOg==
    +  dependencies:
    +    entities "^1.1.1"
    +
     any-promise@^1.1.0:
       version "1.3.0"
       resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
    @@ -2373,6 +2380,11 @@ enhanced-resolve@^4.1.0:
         memory-fs "^0.4.0"
         tapable "^1.0.0"
     
    +entities@^1.1.1:
    +  version "1.1.2"
    +  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
    +  integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
    +
     errno@^0.1.2, errno@^0.1.3, errno@~0.1.7:
       version "0.1.7"
       resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
    @@ -5384,13 +5396,6 @@ std-env@^1.1.0, std-env@^1.3.1:
       dependencies:
         is-ci "^1.1.0"
     
    -stream-ansi2html@^1.1.0:
    -  version "1.1.0"
    -  resolved "https://registry.yarnpkg.com/stream-ansi2html/-/stream-ansi2html-1.1.0.tgz#9a9620e81313c065e811aae963b23b962924d026"
    -  integrity sha1-mpYg6BMTwGXoEarpY7I7likk0CY=
    -  dependencies:
    -    through2 "^2.0.1"
    -
     stream-browserify@^2.0.1:
       version "2.0.1"
       resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
    @@ -5598,7 +5603,7 @@ terser@^3.8.1:
         source-map "~0.6.1"
         source-map-support "~0.5.6"
     
    -through2@^2.0.0, through2@^2.0.1:
    +through2@^2.0.0:
       version "2.0.5"
       resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
       integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
    
    From e3ccd028ccb498e73ecd6a2281f622588ecb9d69 Mon Sep 17 00:00:00 2001
    From: Eric Clemmons 
    Date: Sun, 3 Mar 2019 21:58:49 -0800
    Subject: [PATCH 08/11] Style all buttons the same
    
    ---
     packages/polydev/src/public/styles.css | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/packages/polydev/src/public/styles.css b/packages/polydev/src/public/styles.css
    index 03c3ee7..dcb0ca6 100644
    --- a/packages/polydev/src/public/styles.css
    +++ b/packages/polydev/src/public/styles.css
    @@ -75,7 +75,7 @@
           }
     
           section.error-page .fab,
    -      section.error-page button {
    +      button {
             background: rgb(250, 250, 250);
             border: 1px solid white;
             border-radius: 100em;
    
    From 11ac7ff79660ac7ed12ebb93c17ca9f0dc12f389 Mon Sep 17 00:00:00 2001
    From: Eric Clemmons 
    Date: Sun, 3 Mar 2019 22:28:18 -0800
    Subject: [PATCH 09/11] Enable HMR for routes, even if they don't export
     functions
    
    ---
     .../polydev/src/middleware/router/launcher.js  | 18 ++++++++----------
     1 file changed, 8 insertions(+), 10 deletions(-)
    
    diff --git a/packages/polydev/src/middleware/router/launcher.js b/packages/polydev/src/middleware/router/launcher.js
    index d60c312..293d242 100644
    --- a/packages/polydev/src/middleware/router/launcher.js
    +++ b/packages/polydev/src/middleware/router/launcher.js
    @@ -47,13 +47,12 @@ async function startHandler() {
       if (module.hot) {
         let recentlySaved = false
     
    -    if (typeof handler === "function") {
    -      // @ts-ignore
    -      module.hot.accept(handlerPath, async () => {
    -        if (recentlySaved) {
    -          console.log(`♻️  Restarting ${handlerPath}`)
    -          return process.send("restart")
    -        }
    +    // @ts-ignore
    +    module.hot.accept(handlerPath, async () => {
    +      if (recentlySaved) {
    +        console.log(`♻️  Restarting ${handlerPath}`)
    +        return process.send("restart")
    +      }
     
             handler = await getLatestHandler()
             console.log(`🔁  Hot-reloaded ${handlerPath}`)
    @@ -65,9 +64,8 @@ async function startHandler() {
             // Outside of double-save reload window
             setTimeout(() => {
               recentlySaved = false
    -        }, 500)
    -      })
    -    }
    +      }, 500)
    +    })
       }
     
       const url = `http://localhost:${PORT}/`
    
    From aae647f27a71562d53c16ea7c0a77a9dcf284e46 Mon Sep 17 00:00:00 2001
    From: Eric Clemmons 
    Date: Sun, 3 Mar 2019 22:28:53 -0800
    Subject: [PATCH 10/11] Clear require.cache so that HMR doesn't preserve old
     working version
    
    ---
     packages/polydev/src/middleware/router/launcher.js | 3 +++
     1 file changed, 3 insertions(+)
    
    diff --git a/packages/polydev/src/middleware/router/launcher.js b/packages/polydev/src/middleware/router/launcher.js
    index 293d242..d6c1fad 100644
    --- a/packages/polydev/src/middleware/router/launcher.js
    +++ b/packages/polydev/src/middleware/router/launcher.js
    @@ -30,6 +30,9 @@ const verify = (req, res, buffer, encoding = "utf8") => {
     // TODO Remove baseUrl unless it's needed in the route
     async function startHandler() {
       const getLatestHandler = async () => {
    +    // Best way to ensure that HMR doesn't save old copies
    +    delete require.cache[handlerPath]
    +
         const exported = require(handlerPath)
         const handler = exported ? await (exported.default || exported) : exported
     
    
    From 2f38cc10b7e24aa90937e8cdcf6043431e1a3a89 Mon Sep 17 00:00:00 2001
    From: Eric Clemmons 
    Date: Sun, 3 Mar 2019 22:29:21 -0800
    Subject: [PATCH 11/11] Bubble-up errors when handler can't be require'd
    
    ---
     .../polydev/src/middleware/router/launcher.js | 32 +++++++++++--------
     1 file changed, 18 insertions(+), 14 deletions(-)
    
    diff --git a/packages/polydev/src/middleware/router/launcher.js b/packages/polydev/src/middleware/router/launcher.js
    index d6c1fad..bbde4f7 100644
    --- a/packages/polydev/src/middleware/router/launcher.js
    +++ b/packages/polydev/src/middleware/router/launcher.js
    @@ -57,16 +57,16 @@ async function startHandler() {
             return process.send("restart")
           }
     
    -        handler = await getLatestHandler()
    -        console.log(`🔁  Hot-reloaded ${handlerPath}`)
    +      handler = await getLatestHandler()
    +      console.log(`🔁  Hot-reloaded ${handlerPath}`)
     
    -        // TODO Send reload signal
    +      // TODO Send reload signal
     
    -        // Wait for a double-save
    -        recentlySaved = true
    -        // Outside of double-save reload window
    -        setTimeout(() => {
    -          recentlySaved = false
    +      // Wait for a double-save
    +      recentlySaved = true
    +      // Outside of double-save reload window
    +      setTimeout(() => {
    +        recentlySaved = false
           }, 500)
         })
       }
    @@ -86,12 +86,16 @@ async function startHandler() {
             route,
             // Make sure we always evaluate at run-time for the latest HMR'd handler
             function handleRoute(req, res, next) {
    -          const handled = handler(req, res, next)
    -
    -          // Automatically bubble up async errors
    -          if (handled && handled.catch) {
    -            handled.catch(next)
    -          }
    +          getLatestHandler()
    +            .then((handler) => {
    +              const handled = handler(req, res, next)
    +
    +              // Automatically bubble up async errors
    +              if (handled && handled.catch) {
    +                handled.catch(next)
    +              }
    +            })
    +            .catch(next)
             }
           )
         })