diff --git a/packages/polydev/package.json b/packages/polydev/package.json index 79ede6d..be6825e 100644 --- a/packages/polydev/package.json +++ b/packages/polydev/package.json @@ -11,7 +11,7 @@ "src" ], "dependencies": { - "body-parser": "^1.18.3", + "ansi-to-html": "^0.6.10", "chokidar": "^2.0.4", "debug": "^4.1.1", "express": "^4.16.4", @@ -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/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 08377c9..2d10fc9 100644 --- a/packages/polydev/src/middleware/error/index.js +++ b/packages/polydev/src/middleware/error/index.js @@ -1,32 +1,73 @@ +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 - res.status(statusCode).send(` - - - + const youch = new Youch(error, req) + youch.addLink((error) => { + return ` +
+ ` + }) + + 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)}` + + return `` + }) + + youch.addLink(({ message }) => { + const url = `https://stackoverflow.com/search?q=${encodeURIComponent( + message + )}` + + return `` + }) + + youch.toHTML().then((html) => { + res.status(statusCode).send(html) + }) -
-
-

- ${statusCode} ${status} -

- -
${error.message}
-
- - ${ - error.stack - ? ` - - ` - : "" - } -
- - `) + youch + .toJSON() + .then(forTerminal) + .then(console.log) } 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 65f76d6..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, @@ -110,7 +109,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 bf52182..bbde4f7 100644 --- a/packages/polydev/src/middleware/router/launcher.js +++ b/packages/polydev/src/middleware/router/launcher.js @@ -21,9 +21,18 @@ 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 () => { + // 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 @@ -31,33 +40,35 @@ 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) { 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}`) + 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 - }, 500) - }) - } + // Wait for a double-save + recentlySaved = true + // Outside of double-save reload window + setTimeout(() => { + recentlySaved = false + }, 500) + }) } const url = `http://localhost:${PORT}/` @@ -65,18 +76,26 @@ 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, route, // Make sure we always evaluate at run-time for the latest HMR'd handler - (req, res, next) => { - const handled = handler(req, res, next) - - // Automatically bubble up async errors - if (handled.catch) { - handled.catch(next) - } + function handleRoute(req, res, next) { + getLatestHandler() + .then((handler) => { + const handled = handler(req, res, next) + + // Automatically bubble up async errors + if (handled && handled.catch) { + handled.catch(next) + } + }) + .catch(next) } ) }) diff --git a/packages/polydev/src/public/styles.css b/packages/polydev/src/public/styles.css index 1f184d6..dcb0ca6 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,46 @@ overflow: auto; } + section.error-page .fab, + 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 { + background: rgba(100%, 100%, 100%, 0.5) + } + + section.request-details { + background: white; + box-shadow: 0 5em 10em black + } + section header, section main, section footer { @@ -102,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..115c335 --- /dev/null +++ b/packages/polydev/src/routes/_polydev/install-module/index.js @@ -0,0 +1,62 @@ +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`) + } + + 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.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) {
+      res.write(`
+        
+      `)
+    }
+
+    res.end()
+  })
+}
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 +

    +
    +
    + + `) +} diff --git a/yarn.lock b/yarn.lock index b249c25..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" @@ -2022,7 +2029,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= @@ -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" @@ -4016,6 +4028,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 +5356,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 +6147,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"