├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── README.md ├── bin └── polydev ├── examples ├── 404 │ └── package.json ├── 500 │ ├── package.json │ └── routes │ │ └── index.js ├── apollo-server │ ├── package.json │ └── routes │ │ └── index.js ├── custom-server │ ├── package.json │ ├── routes │ │ └── index.js │ └── server.js ├── dev-only │ ├── package.json │ └── routes │ │ └── index.js ├── express │ ├── package.json │ └── routes │ │ └── index.js ├── graphql │ ├── package.json │ └── routes │ │ ├── index.js │ │ └── schema.js ├── logo │ ├── package.json │ └── routes │ │ └── index.get.js ├── missing-module │ ├── package.json │ └── routes │ │ └── index.js ├── next │ ├── package.json │ ├── pages │ │ └── index.js │ └── routes │ │ └── index.*.js ├── params │ ├── package.json │ └── routes │ │ ├── index.js │ │ └── users │ │ └── :name │ │ └── index.js ├── parcel │ ├── .gitignore │ ├── package.json │ ├── routes │ │ └── index.js │ └── src │ │ ├── ParcelExample.js │ │ ├── index.html │ │ └── index.js ├── sse │ ├── package.json │ └── routes │ │ └── index.*.js └── typescript │ ├── package.json │ ├── routes │ └── index.tsx │ └── tsconfig.json ├── logo.png ├── package.json ├── polydev.gif ├── routes └── index.js ├── scripts ├── dev.js └── start.js ├── src ├── index.js ├── middleware │ ├── assets │ │ └── index.js │ ├── error │ │ └── index.js │ ├── index.js │ ├── notFound │ │ └── index.js │ └── router │ │ ├── bridge.js │ │ ├── createRouterFromFiles.js │ │ ├── findAvailablePort.js │ │ ├── handle.development.js │ │ ├── handle.js │ │ ├── handle.production.js │ │ ├── index.development.js │ │ ├── index.js │ │ ├── index.production.js │ │ └── launcher.js ├── public │ ├── styles.css │ └── triangilify.svg ├── routes │ └── _polydev │ │ └── install-module │ │ └── index.js └── server.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | *.log 3 | examples/**/*.lock 4 | dist 5 | node_modules 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## UNRELEASED 4 | 5 | ##### Pull Requests 6 | 7 | - #19 - Support "-r" flags via child*process.fork/spawn? *(@ericclemmons)\_ 8 | - #7 - TypeScript support _(@ericclemmons)_ 9 | 10 | #### Commits 11 | 12 | - cd9f1f86589c9ccabea5e909d49ba7e8c05600af - yarn dev uses ts-node (@ericclemmons) 13 | - 37c7a93c2984b8c6d85bd050d6acf27b89ae9da8 - @ts-ignore (@ericclemmons) 14 | - 515d9e0d376fe20a5bee50e77aa00ab7a5a2fc70 - Update README.md with updated args example (@ericclemmons) 15 | - 4970d218338604b45a5b653cb55919848fdc7111 - polydev forks to server so we can re-use Node flags (@ericclemmons) 16 | - aebd7ff3ec38e2a0497838111743a483f10bfdbe - Add docs for TypeScript (@ericclemmons) 17 | - 544e4cdc78fa6cbc3f60f6eb948fba9b582a1eab - Routes support .ts (@ericclemmons) 18 | - 9bad30fc1dcff70aa3ecb5a9bf6ea1af5fa96679 - Add typescript-example (@ericclemmons) 19 | - 5c0f950f8557bdb481f17871e334f621f488e344 - typo (@ericclemmons) 20 | - 7d4c0c29f7f3fa3ecc86111ed8a79b4532417f6b - Put gif in blockquote (@ericclemmons) 21 | - 087da74dcaf797d93ca7e667229a423db2bdd42d - Use markdown for links (@ericclemmons) 22 | - ecc074869b7c2b8f3a25da924507036e15a2e663 - Add demo gif (@ericclemmons) 23 | 24 | ## v1.2.0 (2019-01-17) 25 | 26 | ##### Pull Requests 27 | 28 | - #10 - development/production-only routes _(@ericclemmons)_ 29 | - #4 - Production mode _(@ericclemmons)_ 30 | - #16 - Add shadow to logo _(@ericclemmons)_ 31 | 32 | #### Commits 33 | 34 | - f4780730699ea175495274c9c4f00c7d6873487e - In production, don't add routes if no handler is exported (@ericclemmons) 35 | - b73c06b85748c44cd8b9679666336455734a7ab4 - In development, listen to non-exported servers so we at least 404 (@ericclemmons) 36 | - 9bf29abba82ec5122d970a99318f004d05bcea5d - Add /dev-only page (@ericclemmons) 37 | - 62a81a5905553a9a75d825e9c0a867d982b1766b - Add a standalone server.js via dev:server & start:server (@ericclemmons) 38 | - 31052bbb085ad275f2203a6691cb68cd26145c34 - Export { polydev} as a middleware (@ericclemmons) 39 | - 0bd0883bf6800f4fbc3afc767f2b283c6260b905 - Add shadow to logo (@ericclemmons) 40 | 41 | ## v1.1.0 (2019-01-16) 42 | 43 | ##### Pull Requests 44 | 45 | - #11 - Make assets available under _polydev _(@ericclemmons)\_ 46 | - #12 - Empty repo experience _(@ericclemmons)_ 47 | 48 | #### Commits 49 | 50 | - 698945d6cf194d4a8100589a1c1a852e9ef39f0a - Some quick documentation on how to use it (@ericclemmons) 51 | - 8a7bbe39ffdc5348b0022d7f4f92622ad0fb3474 - All polydev-specific styles are under /\_polydev (@ericclemmons) 52 | - 258c52099943b8dd38d3bee55e2299677a9d2b81 - Use path.join for potential path (@ericclemmons) 53 | 54 | ## v1.0.0 (2019-01-16) 55 | 56 | #### Commits 57 | 58 | - 056e990f9298dbc7cacd7e642be9e320436ff6f9 - np --yolo (@ericclemmons) 59 | - ae7eec541b41b785f04dc35bd0168710819869b0 - Revert "Move np dependency to root" (@ericclemmons) 60 | - cab50b2bc1e6db44b9d5838f9be931287a7bf0eb - Remove unlink on preinstall, since this can fail when the link doesn't exist (@ericclemmons) 61 | - 9e99e2fe6c416747df1beae1ffaf787082ef4926 - Move np dependency to root (@ericclemmons) 62 | - 9a462afac86c26794472c28ff271b713cbb953c5 - Prepare for publishing (@ericclemmons) 63 | - 04472ddd39d1dd18776e11ee8248a0a5bb8dcdba - Start at v0.0.0 (@ericclemmons) 64 | - 268cb6644051cc78fd4343a44f67d257ddc31de5 - unlink before link (@ericclemmons) 65 | - 9cad508cd5e7f1ac9d30f0627a3ff6784599c33e - Ignore dist (@ericclemmons) 66 | - 254beecbae77636d78a3b2b58ca179ebb1db5001 - Link Roadmap to GitHub issues (@ericclemmons) 67 | - 66902b3a6b182e99c5f5b2eee35f579b1e634111 - Better README.md (@ericclemmons) 68 | - f85b332109ffc7e91826b1472c211ed3aede0a7d - Add /logo for capturing the logo (@ericclemmons) 69 | - 02072a945f8759ffccaedf8cc1f49d9700d61016 - Forward most of process.env to sub-processes (@ericclemmons) 70 | - c26e4c518a7ad736f3c3ef7560075464e2ec6f61 - Fix reference to NODE_ENV (@ericclemmons) 71 | - 7de772aa29e2f251ecdcfc37f4f71e082a2860e8 - build next-example before starting (@ericclemmons) 72 | - 454fe9d3ec151c2c596a674c761fc3e6fa58d8fd - production mode (@ericclemmons) 73 | - e8f1431047359ca7ec7a5f538a5c88cddb052113 - Enable notFound & error routes only in development (@ericclemmons) 74 | - 183aa8ee23fae27e8b292f7bddae138ccc228c54 - Enable apollo-playground in production (@ericclemmons) 75 | - 3376f8718d7fdac592c3147e2b4bdbdf63cbb086 - Remove esm (@ericclemmons) 76 | - ed324708968fe1e342588836443f15d8ed934438 - yarn dev & yarn start (@ericclemmons) 77 | - 7bb3c61e30377bd34fec6e148dbe6e59f2754194 - Prettier 404 page (@ericclemmons) 78 | - f62bb5cf959f8f272a6a01363f6138ad32c188dd - Fix bug with index.js not using both GET & POST (@ericclemmons) 79 | - 659c58d5cd0b0e4490e76fd49d5701963cc07a9f - GraphQL doesn't need to use express explicitly (@ericclemmons) 80 | - dc02fc5096be1502d570644094d88516b7d112dd - Routes are duplicated between polydev + launcher (so that express behaves the same) (@ericclemmons) 81 | - 49ec7fe2c07c42dee22de32ae7de53d21776af81 - Add better parameterized route example (@ericclemmons) 82 | - 15a9c4c695c4809d05ea277347dde4d3213a2b5a - No longer namespace routes automatically (we'll see) (@ericclemmons) 83 | - d07edb4e2435c6d7e8ce04ca00fd09b5c7e29d34 - Prettier error page (@ericclemmons) 84 | - f0854e4ced4330d2b93b6f5af951388191fa4616 - hot-module-replacement@^3.0.3 fixes Next.js (reverts f41663680b577f1dc67906eef57f7212e875ed80) (@ericclemmons) 85 | - 7d79c3e3f5f914764a5e5dca17cd51661fc38cf1 - Add helpful postinstall + start scripts (@ericclemmons) 86 | - f41663680b577f1dc67906eef57f7212e875ed80 - Use fork until new version is published with https://github.com/sidorares/hot-module-replacement/pull/14 (@ericclemmons) 87 | - aa8c2f3c7fdc617224b1711d6b590f18f0c93d65 - 500 error example (@ericclemmons) 88 | - 2ddc2161d697f289e0dec1a9b9fa45db99887718 - Interactive Next.js example (@ericclemmons) 89 | - f57bb7df748ccf24b22bdd45c67dace8df5f5f95 - Prettier Next.js example (@ericclemmons) 90 | - 137b17c9283c10c5ac26b7174fca38937d1a2635 - Absolute path to /styles.css (@ericclemmons) 91 | - 27d8a6ac4ff8da41baa2bbf15fc1f83d9b1f04f3 - Working SSE example! This means Next.js works too! (@ericclemmons) 92 | - 27c37cdd07ba808372b1ea73948364e19d58eb9b - whitespace (@ericclemmons) 93 | - 1959a9906d12736c36783383bdebaf3909066149 - SSE example (@ericclemmons) 94 | - 5f53d1e2cc7319ce8fc96c8ad156c35059bca038 - Revert "bridge returns a promise instead of talking to the process directly" (@ericclemmons) 95 | - c08a9ff2d32e08ce10548d3e257fdf37bff919f8 - Add notes for error page (@ericclemmons) 96 | - 4ca11f87d7cbf401bffb2296b7534aab45e4a4ea - Remove polydev exports (@ericclemmons) 97 | - 063f4c8f5da3ee4adb0397ee9ea313f3d6b11d67 - Use body-parser for 404 page (@ericclemmons) 98 | - c516b73362af774f4cbe7bc793d3a4d0a7448af0 - Remove redundant variables (@ericclemmons) 99 | - c78e02f09776c32e78f59d85efbfeff96bcd275a - Switch to support index[.*|get|post].js (@ericclemmons) 100 | - 65f7f55147e23802a87f2c8430276a5e9aa856aa - Routes only respond to GET/POST for now (@ericclemmons) 101 | - f2e67043fdf25d35c1c87de6f5f3056f1fe0ef4f - Enable built-in HMR only for functional routes (@ericclemmons) 102 | - db7ad56550fb94a7bbe4c00fa0a3eb46e1f8a0a2 - Got pages returning from Next.js (@ericclemmons) 103 | - 6af6705cb7111aae12716d5abd2d2fb43a31dfa4 - If a child process crashes, re-create it on the next request (@ericclemmons) 104 | - 7f9471fad71c359a4eea0afb9fedb376a4a1518c - Revert 208088222dd4e14c68d0b6e2b42e31e0ffb224b9 to prevent crashing the process (@ericclemmons) 105 | - a829c90724edc8bd70a46a240ba427adb1f416df - Working apollo-server example (@ericclemmons) 106 | - deaefcf259d0e5b82e540c855ffc7f6b81df1e11 - Simpler child exists check (@ericclemmons) 107 | - 208088222dd4e14c68d0b6e2b42e31e0ffb224b9 - Remove process.on errors (@ericclemmons) 108 | - c6b99bcb0178276545f705c0d16e95d065267bad - Add note for bug with HMR with servers (@ericclemmons) 109 | - c6d65f874de1067e863e4112f634eef1a59dd4d1 - Fix regression with HMR not referencing the latest handler (@ericclemmons) 110 | - bf68ccf3abdb8dd70461b24a9d5b16efcaebb881 - require("polydev").mount(handler) is good for relative routes (@ericclemmons) 111 | - 99e5d30c6e9175671812d0d28c94b4806aee431b - Move server to separate file (@ericclemmons) 112 | - 0ffdc17b8765c8d9c283f1b646d28ae0addc5e05 - Add hit counter to express example (@ericclemmons) 113 | - b35915642a223250a2397a4ddd719d481d423020 - Fix background (@ericclemmons) 114 | - e12e243a4f908a41ea435e084d8611a7271a8041 - Use font-size: 16px (@ericclemmons) 115 | - 8b9c72ef7fa4cef95e82dff078db02737fde4195 - bridge returns a promise instead of talking to the process directly (@ericclemmons) 116 | - d0d50186a37a74cb7e3f1f3f2d65d64cd240a5b9 - Force arrow-parens (@ericclemmons) 117 | - 42e81e5fb1efa88b28d06732ffe471082e3f9ff6 - Double-save to restart server (@ericclemmons) 118 | - 05437679fb023c65611d9fa84e877ce8a86ec11a - hot-module-replace is legit! (@ericclemmons) 119 | - 971fee30a995299e2aec693b090effcc9e5137ba - Remove form margins (@ericclemmons) 120 | - ce415d88e897551a83647c8cc484be6a9844a371 - Add note about empty routes (@ericclemmons) 121 | - b04ff3f875f1655e8a5b51388aa4e3481664a082 - Create empty pages with _some_ content (@ericclemmons) 122 | - 748c15b2269268e775af671c5373a6795b43449b - Wait for the servers to start before finishing the request (@ericclemmons) 123 | - efdd1e4adb74c7786f7b759f8899319afc9e22f9 - Prettier 404 page (@ericclemmons) 124 | - 45e6bc7dc9e702e1234e93afe59d34218de30804 - Events are tied to their request/response via a UUID (@ericclemmons) 125 | - 75b932bb0a9cbf0feb87289a2a2cad3b5216894f - 404 page lets you create the missing page (@ericclemmons) 126 | - 35984d98a74541d9c0ad08c3e36b2e31d6365b69 - Add Next.js example (@ericclemmons) 127 | - 47bc36f7dc19b3f9cb9a978b5892d029c00fe2ec - Faster fadeIn (@ericclemmons) 128 | - fe9ebec00065547f1f7000cc72126383f1311cd1 - apollo-server 2 is terrible (@ericclemmons) 129 | - 0668c6083c7becbc1e999de41ad92b884febed72 - Add graphql-example (@ericclemmons) 130 | - e7fb252240ae620336eee176e3734af6f714ec0e - Improved folder structure (@ericclemmons) 131 | - 819c4fed7b900cfc4e8c19a21653fdf7f1be3f19 - Only open window if --open (@ericclemmons) 132 | - 9c7d76901532121dd50f09c53c1a8227dc3c7fa3 - Use express internally (@ericclemmons) 133 | - 721f0340c647e9c2101845c0e6cd0e929d0fdadd - Add express-example (@ericclemmons) 134 | - 6d4fa253f344acd09cbf609ce695884dcab77c68 - Add 404 link (@ericclemmons) 135 | - 1a5914c738a4caabac65d8d02e6bccf0ebd97ddd - Add (broken) apollo-server-example (@ericclemmons) 136 | - 60d061cf35c9e321b0eacbc64fb9d701d57cbc45 - Have a pretty example page (@ericclemmons) 137 | - 7f4b0f41f3455ae854fd820b3ea5211d166b9bba - Support static assets (@ericclemmons) 138 | - 14f793ef9e3d3770ba7201688aa225feedb4f4b7 - Add a better README.md (@ericclemmons) 139 | - 41739c49d7f4de73d13e96fa6deb431b5211339c - Switch to yarn workspaces (@ericclemmons) 140 | - 60b23499d574446c56be2704c2fe30f9076035fb - opn URL when the server starts (@ericclemmons) 141 | - 73bb8d12eaebd74bb7124b84612bad099faee351 - Split out lambda (@ericclemmons) 142 | - 2d5a04753d4538ad5a9f4acb9a479e6c274680ba - Simple node handler (@ericclemmons) 143 | 144 | --- 145 | 146 | Automatically generated by `🤖 CHANGEBOT`. 147 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ![polydev](/logo.png) 2 | 3 | # Contributing 4 | 5 | ## How to Work on Polydev 6 | 7 | 1. Fork & clone this repo, as usual. 8 | 1. `yarn install` 9 | 10 | _(This will install the `polydev` binary globally.)_ 11 | 12 | 1. `yarn dev EXAMPLE_NAME` 13 | 14 | _(This is for "development" mode)_ 15 | 16 | 1. `yarn start EXAMPLE_NAME` 17 | 18 | _(This is for "production" mode)_ 19 | 20 | ## How to Publish 21 | 22 | 1. `yarn release` 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![polydev](/logo.png) 2 | 3 | > Faster, route-centric development for [Node.js][node] apps with built-in 4 | > [Hot Module Replacement][hmr]. 5 | > 6 | > ![Demo GIF](/polydev.gif) 7 | 8 | ## Rationale 9 | 10 | As your project grows, **working on a large or monolithic [Node.js][node] app gets slower**: 11 | 12 | - Working on _part_ of the app means running the _entire_ app. 13 | - The `require` tree grows so large it can take several seconds to start the server. 14 | - Restarting the server on every change impedes development. 15 | - Middleware for projects like [Next.js][next] & [Storybook][storybook] are expensive 16 | to restart with each change. 17 | - Tools like [concurrently][concurrently], [nodemon][nodemon], & [piping][piping] still 18 | run the entire app. 19 | - You shouldn't waste time in the terminal hitting Ctrl-C and restarting. 20 | 21 | ## Features 22 | 23 | - Fast startup. 24 | - [Hot Module Replacement][hmr] built-in. 25 | - Run only the parts of your app that's requested. 26 | - Supports [yarn workspaces][workspaces]. 27 | - Pretty `404` screens with the option to create the missing route. 28 | - Pretty `500` screens, so you spend less time in the terminal. 29 | - Iterative adoption, so it's easy to get started. 30 | 31 | ## Quick Started 32 | 33 | 1. Install 34 | 35 | ```shell 36 | yarn add polydev --dev 37 | ``` 38 | 39 | 2. Run `polydev` 40 | 41 | ```shell 42 | yarn run polydev --open 43 | ``` 44 | 45 | For customizing the `node` runtime, you can use `NODE_OPTIONS`. 46 | 47 | For example, [TypeScript][typescript] can be enabled via [ts-node][ts-node]: 48 | 49 | ```shell 50 | polydev --require ts-node/register 51 | # Or 52 | NODE_OPTIONS="--require ts-node/register" polydev 53 | ``` 54 | 55 | ## Defining `routes` 56 | 57 | The `routes` folder is similar to Old-Time™ HTML & PHP, where 58 | **folders mirror the URL structure**, followed by an `index.js` file: 59 | 60 | - `routes/` 61 | 62 | - `page/[id]/index.js` 63 | 64 | _Has access to `req.params.id` for [/page/123](http://localhost:3000/page/123)._ 65 | 66 | - `contact-us/` 67 | 68 | - `index.get.js` 69 | - `index.post.js` 70 | 71 | - `posts/index.*.js` 72 | 73 | _Responds to both `GET` & `POST` for [/posts/\*](http://localhost:3000/posts)._ 74 | 75 | - `index.js` 76 | 77 | _Responds to both `GET` & `POST` for [/](http://localhost:3000/)._ 78 | 79 | ### Route Handlers 80 | 81 | Route handlers can be any of the following: 82 | 83 | 1. Functional middleware: 84 | 85 | ```js 86 | module.exports = (req, res) => { 87 | res.send("Howdy there!") 88 | } 89 | ``` 90 | 91 | 2. Express apps: 92 | 93 | ```js 94 | const express = require("express") 95 | 96 | module.exports = express().get("/", (req, res) => { 97 | res.send(`Howdy from ${req.path}!`) 98 | }) 99 | ``` 100 | 101 | 3. A [yarn workspace][workspaces] package: 102 | 103 | ```js 104 | module.exports = require("my-package-name") 105 | ``` 106 | 107 | 4. A `package.json` path: 108 | 109 | ```js 110 | module.exports = require.resolve("my-app/package.json") 111 | ``` 112 | 113 | These are considered stand-alone apps that will be ran via `yarn dev` or `yarn start` (whichever exists) for development only. 114 | 115 | This is good for when you want to have a separate API server open on `process.env.PORT` that's not part of your application. 116 | 117 | ## Contributing 118 | 119 | > See [CONTRIBUTING.md](/CONTRIBUTING.md). 120 | 121 | ## Author 122 | 123 | - [Eric Clemmons][twitter] 124 | 125 | [concurrently]: https://github.com/kimmobrunfeldt/concurrently 126 | [hmr]: https://github.com/sidorares/hot-module-replacement 127 | [issues]: https://github.com/ericclemmons/polydev/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc 128 | [next]: https://github.com/zeit/next.js/ 129 | [node]: https://nodejs.org/ 130 | [nodemon]: https://github.com/remy/nodemon 131 | [piping]: https://www.npmjs.com/package/piping 132 | [storybook]: https://github.com/storybooks/storybook 133 | [ts-node]: https://github.com/TypeStrong/ts-node 134 | [typescript]: https://www.typescriptlang.org/ 135 | [twitter]: https://twitter.com/ericclemmons 136 | [workspaces]: https://yarnpkg.com/en/docs/workspaces 137 | -------------------------------------------------------------------------------- /bin/polydev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Default to "development" by convention 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = "development" 6 | } 7 | 8 | const child_process = require("child_process") 9 | const path = require("path") 10 | 11 | const serverPath = path.resolve(__dirname, "../src/server.js") 12 | const [, , ...args] = process.argv 13 | 14 | // Remove polydev custom flags 15 | const execArgv = args.filter(arg => !["--open"].includes(arg)) 16 | 17 | // Spawn server via node + flags 18 | child_process.fork(serverPath, args, { 19 | execArgv 20 | }) 21 | -------------------------------------------------------------------------------- /examples/404/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "404-example", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "polydev", 7 | "start": "NODE_ENV=production polydev" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/500/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "500-example", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "polydev", 7 | "start": "NODE_ENV=production polydev" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/500/routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res) => { 2 | throw new Error("💥 Whoopsiedoodle! 🤷‍♂️ ") 3 | } 4 | -------------------------------------------------------------------------------- /examples/apollo-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "apollo-server-example", 4 | "version": "0.0.0", 5 | "dependencies": { 6 | "apollo-server": "^2.3.1", 7 | "graphql": "^14.0.2" 8 | }, 9 | "scripts": { 10 | "dev": "polydev", 11 | "start": "NODE_ENV=production polydev" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/apollo-server/routes/index.js: -------------------------------------------------------------------------------- 1 | const { ApolloServer, gql } = require("apollo-server-express") 2 | const express = require("express") 3 | 4 | // This is a (sample) collection of books we'll be able to query 5 | // the GraphQL server for. A more complete example might fetch 6 | // from an existing data source like a REST API or database. 7 | const books = [ 8 | { 9 | title: "Harry Potter and the Chamber of Secrets", 10 | author: "J.K. Rowling" 11 | }, 12 | { 13 | title: "Jurassic Park", 14 | author: "Michael Crichton" 15 | } 16 | ] 17 | 18 | // Type definitions define the "shape" of your data and specify 19 | // which ways the data can be fetched from the GraphQL server. 20 | const typeDefs = gql` 21 | # Comments in GraphQL are defined with the hash (#) symbol. 22 | 23 | # This "Book" type can be used in other type declarations. 24 | type Book { 25 | title: String 26 | author: String 27 | } 28 | 29 | # The "Query" type is the root of all GraphQL queries. 30 | # (A "Mutation" type will be covered later on.) 31 | type Query { 32 | books: [Book] 33 | } 34 | ` 35 | 36 | // Resolvers define the technique for fetching the types in the 37 | // schema. We'll retrieve books from the "books" array above. 38 | const resolvers = { 39 | Query: { 40 | books: () => books 41 | } 42 | } 43 | 44 | // In the most basic sense, the ApolloServer can be started 45 | // by passing type definitions (typeDefs) and the resolvers 46 | // responsible for fetching the data for those types. 47 | const apollo = new ApolloServer({ 48 | // Enable playground when NODE_ENV=production 49 | playground: true, 50 | typeDefs, 51 | resolvers 52 | }) 53 | 54 | const app = express() 55 | 56 | // Mount GraphQL at the root, not `/graphql` by default 57 | apollo.applyMiddleware({ app, path: "/" }) 58 | 59 | module.exports = app 60 | -------------------------------------------------------------------------------- /examples/custom-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "custom-server-example", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "polydev", 7 | "start": "NODE_ENV=production polydev" 8 | }, 9 | "dependencies": { 10 | "express": "^4.16.4" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/custom-server/routes/index.js: -------------------------------------------------------------------------------- 1 | let hits = 0 2 | 3 | module.exports = (req, res) => { 4 | hits++ 5 | 6 | res.send(` 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 |

15 | 👋 Howdy from a custom-server 16 |

17 | 18 |

19 | ${hits} ${hits ? "hits" : "hit"} 20 |

21 |
22 | 23 | 26 |
27 | `) 28 | } 29 | -------------------------------------------------------------------------------- /examples/custom-server/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express") 2 | const { polydev } = require("polydev") 3 | 4 | const app = express().use( 5 | polydev({ 6 | assets: "public", 7 | routes: "routes" 8 | }) 9 | ) 10 | 11 | const server = app.listen(process.env.PORT || 3000, () => { 12 | const url = `http://localhost:${server.address().port}/` 13 | 14 | console.log(`🚀 Ready! ${url}`) 15 | }) 16 | -------------------------------------------------------------------------------- /examples/dev-only/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "dev-only-example", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "polydev", 7 | "start": "NODE_ENV=production polydev" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/dev-only/routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | process.env.NODE_ENV === "development" 3 | ? (req, res) => { 4 | res.send(` 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 |
15 |

🤫 Shhhh!

16 | 17 |

18 | This page is only available in development. 19 |

20 |
21 |
22 | 23 | `) 24 | } 25 | : undefined 26 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "express-example", 4 | "version": "0.0.0", 5 | "dependencies": { 6 | "express": "^4.16.4" 7 | }, 8 | "scripts": { 9 | "dev": "polydev", 10 | "start": "NODE_ENV=production polydev" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/express/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express") 2 | 3 | let hits = 0 4 | 5 | module.exports = express().get("/", (req, res) => { 6 | hits++ 7 | 8 | res.send(` 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 |

17 | 👋 Howdy from express 18 |

19 | 20 |

21 | ${hits} ${hits ? "hits" : "hit"} 22 |

23 |
24 | 25 | 28 |
29 | `) 30 | }) 31 | -------------------------------------------------------------------------------- /examples/graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "graphql-example", 4 | "version": "0.0.0", 5 | "dependencies": { 6 | "express-graphql": "^0.7.1", 7 | "graphql": "^14.0.2", 8 | "graphql-tools": "^4.0.3" 9 | }, 10 | "scripts": { 11 | "dev": "polydev", 12 | "start": "NODE_ENV=production polydev" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/graphql/routes/index.js: -------------------------------------------------------------------------------- 1 | const graphql = require("express-graphql") 2 | const schema = require("./schema") 3 | 4 | module.exports = graphql({ 5 | graphiql: true, 6 | pretty: true, 7 | schema 8 | }) 9 | -------------------------------------------------------------------------------- /examples/graphql/routes/schema.js: -------------------------------------------------------------------------------- 1 | const { makeExecutableSchema } = require("graphql-tools") 2 | // This is a (sample) collection of books we'll be able to query 3 | // the GraphQL server for. A more complete example might fetch 4 | // from an existing data source like a REST API or database. 5 | const books = [ 6 | { 7 | title: "Harry Potter and the Chamber of Secrets", 8 | author: "J.K. Rowling" 9 | }, 10 | { 11 | title: "Jurassic Park", 12 | author: "Michael Crichton" 13 | } 14 | ] 15 | 16 | // Type definitions define the "shape" of your data and specify 17 | // which ways the data can be fetched from the GraphQL server. 18 | const typeDefs = ` 19 | # Comments in GraphQL are defined with the hash (#) symbol. 20 | 21 | # This "Book" type can be used in other type declarations. 22 | type Book { 23 | title: String 24 | author: String 25 | } 26 | 27 | # The "Query" type is the root of all GraphQL queries. 28 | # (A "Mutation" type will be covered later on.) 29 | type Query { 30 | books: [Book] 31 | } 32 | ` 33 | 34 | // Resolvers define the technique for fetching the types in the 35 | // schema. We'll retrieve books from the "books" array above. 36 | const resolvers = { 37 | Query: { 38 | books: () => books 39 | } 40 | } 41 | 42 | module.exports = makeExecutableSchema({ resolvers, typeDefs }) 43 | -------------------------------------------------------------------------------- /examples/logo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "logo-example", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "polydev", 7 | "start": "NODE_ENV=production polydev" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/logo/routes/index.get.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res) => { 2 | res.send(` 3 | 4 | 5 | 6 | 7 | 36 | 37 | 38 | 39 |
40 |

polydev

41 | 42 | `) 43 | } 44 | -------------------------------------------------------------------------------- /examples/missing-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "missing-module-example", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "polydev", 7 | "start": "NODE_ENV=production polydev" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/missing-module/routes/index.js: -------------------------------------------------------------------------------- 1 | const THIS_PACKAGE_WONT_EXIST = require("THIS_PACKAGE_WONT_EXIST") 2 | 3 | module.exports = (req, res) => { 4 | res.send(` 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 |
15 |

16 | 👋 Howdy from polydev 17 |

18 |
19 |
20 | 21 | `) 22 | } 23 | -------------------------------------------------------------------------------- /examples/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "next-example", 4 | "version": "0.0.0", 5 | "dependencies": { 6 | "next": "^7.0.2", 7 | "react": "^16.7.0", 8 | "react-dom": "^16.7.0" 9 | }, 10 | "scripts": { 11 | "dev": "polydev", 12 | "prestart": "next build", 13 | "start": "NODE_ENV=production polydev" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/next/pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head" 2 | import { Component, Fragment } from "react" 3 | 4 | export default class NextExample extends Component { 5 | state = { taps: 1 } 6 | 7 | render() { 8 | const { taps } = this.state 9 | 10 | return ( 11 | 12 | 13 | 17 | 18 | 19 | 20 |
21 | 22 |
23 |
24 |

25 | 👋 Howdy from Next.js 26 |

27 | 28 |

29 | {taps} {taps ? "taps" : "tap"} 30 |

31 |

32 | 35 |

36 |
37 | 38 | 41 |
42 | 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/next/routes/index.*.js: -------------------------------------------------------------------------------- 1 | const express = require("express") 2 | const next = require("next") 3 | 4 | const dev = process.env.NODE_ENV !== "production" 5 | const pages = next({ dev }) 6 | const handle = pages.getRequestHandler() 7 | 8 | module.exports = pages.prepare().then(() => { 9 | return express().get("*", (req, res) => handle(req, res)) 10 | }) 11 | -------------------------------------------------------------------------------- /examples/params/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "params-example", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "polydev", 7 | "start": "NODE_ENV=production polydev" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/params/routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res) => res.redirect("/users/howdy") 2 | -------------------------------------------------------------------------------- /examples/params/routes/users/:name/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res) => { 2 | const { name } = req.params 3 | 4 | res.send(` 5 | 6 | 7 | 8 |
9 | 10 |
11 |
12 |

13 | 👋 Howdy there ${name}! 14 |

15 | 16 |
17 | 20 |
21 | /users/ 27 | 28 | 29 |
30 | 31 | 40 |
41 | 42 | 45 |
46 | `) 47 | } 48 | -------------------------------------------------------------------------------- /examples/parcel/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | public 3 | -------------------------------------------------------------------------------- /examples/parcel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "parcel-example", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "polydev", 7 | "start": "NODE_ENV=production polydev" 8 | }, 9 | "devDependencies": { 10 | "parcel-bundler": "^1.12.0" 11 | }, 12 | "dependencies": { 13 | "react": "^16.8.4", 14 | "react-dom": "^16.8.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/parcel/routes/index.js: -------------------------------------------------------------------------------- 1 | const Bundler = require("parcel-bundler") 2 | const bundler = new Bundler("./src/index.html", { outDir: "./public" }) 3 | 4 | module.exports = bundler.middleware() 5 | -------------------------------------------------------------------------------- /examples/parcel/src/ParcelExample.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from "react" 2 | 3 | export function ParcelExample() { 4 | const [taps, setTaps] = useState(1) 5 | 6 | return ( 7 | 8 | 12 | 13 | 14 |
15 | 16 |
17 |
18 |

19 | 👋 Howdy from Parcel 20 |

21 | 22 |

23 | {taps} {taps ? "taps" : "tap"} 24 |

25 |

26 | 27 |

28 |
29 | 30 | 33 |
34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /examples/parcel/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/parcel/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { render } from "react-dom" 3 | import { ParcelExample } from "./ParcelExample" 4 | 5 | render(, document.getElementById("root")) 6 | -------------------------------------------------------------------------------- /examples/sse/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "sse-example", 4 | "version": "0.0.0", 5 | "dependencies": { 6 | "@toverux/expresse": "^2.4.0", 7 | "express": "^4.16.4" 8 | }, 9 | "scripts": { 10 | "dev": "polydev", 11 | "start": "NODE_ENV=production polydev" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/sse/routes/index.*.js: -------------------------------------------------------------------------------- 1 | const { sse } = require("@toverux/expresse") 2 | const express = require("express") 3 | const path = require("path") 4 | 5 | const time = () => new Date().toLocaleTimeString("en-US") 6 | 7 | module.exports = express() 8 | .get("/", (req, res) => { 9 | const eventUrl = path.join(req.originalUrl, "time") 10 | 11 | res.send(` 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 |

20 | 👇 Server-Sent Events (SSE) 21 |

22 | 23 |

24 | The time is ${time()} 25 |

26 | 27 |

28 | (View the console for more) 29 |

30 |
31 | 32 | 35 |
36 | 37 | 46 | `) 47 | }) 48 | .get("/time", sse(), (req, res) => { 49 | let messageId = parseInt(req.header("Last-Event-ID"), 10) || 0 50 | 51 | setInterval(() => { 52 | messageId++ 53 | const message = time() 54 | 55 | res.sse.data(message, messageId) 56 | res.sse.event("time", message, messageId) 57 | res.sse.comment(message) 58 | }, 1000) 59 | }) 60 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "typescript-example", 4 | "version": "0.0.0", 5 | "devDependencies": { 6 | "ts-node": "^8.0.3", 7 | "typescript": "^3.3.3333" 8 | }, 9 | "scripts": { 10 | "dev": "polydev --require ts-node/register", 11 | "prestart": "tsc", 12 | "start": "cd dist && NODE_ENV=production polydev" 13 | }, 14 | "dependencies": { 15 | "@types/express": "^4.16.1", 16 | "react": "^16.8.4", 17 | "react-dom": "^16.8.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/typescript/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express" 2 | import * as React from "react" 3 | import { renderToStaticMarkup } from "react-dom/server" 4 | 5 | let hits = 0 6 | 7 | export default (req: Request, res: Response) => { 8 | hits++ 9 | 10 | res.send( 11 | renderToStaticMarkup( 12 | <> 13 | 17 | 18 | 19 |
20 | 21 |
22 |
23 |

24 | 👋 Howdy from TypeScript 25 |

26 | 27 |

28 | {hits} {hits ? "hits" : "hit"} 29 |

30 |
31 | 32 | 35 |
36 | 37 | ) 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "jsx": "react", 5 | "outDir": "dist", 6 | "rootDir": "." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericclemmons/polydev/3f8d9131e5827977dc9fe6a2c8add5967b3452c1/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polydev", 3 | "version": "1.7.1", 4 | "bin": "./bin/polydev", 5 | "main": "./src/index.js", 6 | "repository": "git@github.com:ericclemmons/polydev.git", 7 | "author": "Eric Clemmons ", 8 | "license": "MIT", 9 | "files": [ 10 | "bin", 11 | "src" 12 | ], 13 | "scripts": { 14 | "dev": "./scripts/dev.js", 15 | "start": "./scripts/start.js", 16 | "postinstall": "yarn link", 17 | "release": "np --yolo" 18 | }, 19 | "dependencies": { 20 | "ansi-to-html": "^0.6.10", 21 | "chokidar": "^2.0.4", 22 | "common-tags": "^1.8.0", 23 | "debug": "^4.1.1", 24 | "express": "^4.16.4", 25 | "fs-jetpack": "^2.2.0", 26 | "hot-module-replacement": "^3.0.3", 27 | "hot-replacement-module": "https://github.com/ericclemmons/hot-module-replacement.git", 28 | "opn": "^5.4.0", 29 | "strip-ansi": "^5.1.0", 30 | "uuid": "^3.3.2", 31 | "wait-on": "^3.2.0", 32 | "wait-port": "^0.2.2", 33 | "youch": "^2.0.10", 34 | "youch-terminal": "^1.0.0" 35 | }, 36 | "devDependencies": { 37 | "np": "^4.0.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /polydev.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericclemmons/polydev/3f8d9131e5827977dc9fe6a2c8add5967b3452c1/polydev.gif -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res) => { 2 | res.send(` 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 |
13 |

14 | 👋 Howdy from polydev 15 |

16 | 17 |

Examples

18 | 56 |
57 |
58 | 59 | `) 60 | } 61 | -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { execSync } = require("child_process") 4 | const fs = require("fs") 5 | const path = require("path") 6 | 7 | const [, , example] = process.argv 8 | const examplesDir = path.resolve(__dirname, "../examples") 9 | 10 | if (!example) { 11 | const examples = fs 12 | .readdirSync(examplesDir, "utf8") 13 | .filter((folder) => 14 | fs.statSync(path.resolve(examplesDir, folder)).isDirectory() 15 | ) 16 | 17 | throw new Error(`$ yarn example ${examples}`) 18 | } 19 | 20 | const options = { 21 | cwd: path.resolve(examplesDir, example), 22 | stdio: "inherit" 23 | } 24 | 25 | execSync("yarn install", options) 26 | execSync("yarn dev", options) 27 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { execSync } = require("child_process") 4 | const fs = require("fs") 5 | const path = require("path") 6 | 7 | const [, , example] = process.argv 8 | const examplesDir = path.resolve(__dirname, "../examples") 9 | 10 | if (!example) { 11 | const examples = fs 12 | .readdirSync(examplesDir, "utf8") 13 | .filter((folder) => 14 | fs.statSync(path.resolve(examplesDir, folder)).isDirectory() 15 | ) 16 | 17 | throw new Error(`$ yarn example ${examples}`) 18 | } 19 | 20 | const options = { 21 | cwd: path.resolve(examplesDir, example), 22 | stdio: "inherit" 23 | } 24 | 25 | execSync("yarn install", options) 26 | execSync("yarn start", options) 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express") 2 | const path = require("path") 3 | 4 | const middleware = require("./middleware") 5 | 6 | const { NODE_ENV = "development" } = process.env 7 | 8 | const verify = (req, res, buffer, encoding = "utf8") => { 9 | if (buffer && buffer.length) { 10 | req.rawBody = buffer.toString(encoding) 11 | } 12 | } 13 | 14 | module.exports.polydev = (options = {}) => { 15 | const { assets = "public", routes = "routes" } = options 16 | const app = express() 17 | 18 | // req.body is needed 19 | app.use(express.urlencoded({ extended: true, verify })) 20 | app.use(express.json({ verify })) 21 | 22 | // Ensure polydev assets can be referenced for demos 23 | if (NODE_ENV === "development") { 24 | app.use("/_polydev", middleware.assets(path.resolve(__dirname, "./public"))) 25 | } 26 | 27 | app.use(middleware.assets(assets)) 28 | app.use(middleware.router(routes)) 29 | 30 | // TODO Merge 404 & errors together 31 | if (NODE_ENV === "development") { 32 | app.use(middleware.router(path.resolve(__dirname, "./routes"))) 33 | app.use(middleware.notFound) 34 | app.use(middleware.error) 35 | } 36 | 37 | return app 38 | } 39 | -------------------------------------------------------------------------------- /src/middleware/assets/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express") 2 | 3 | // TODO What are good production settings? 4 | // Sure, there should be a proxy/CDN for static assets, but whatever 5 | module.exports = (...dirs) => { 6 | return dirs.map((dir) => 7 | express.static(dir, { 8 | index: false, 9 | fallthrough: true 10 | }) 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/middleware/error/index.js: -------------------------------------------------------------------------------- 1 | const generateId = require("uuid/v1") 2 | const stripAnsi = require("strip-ansi") 3 | const Youch = require("youch") 4 | const forTerminal = require("youch-terminal") 5 | 6 | const nonce = generateId() 7 | 8 | module.exports = function errorHandler(error, req, res, next) { 9 | const { status = "", statusCode = 500 } = error 10 | 11 | error.message = stripAnsi(error.message) 12 | .split("\n") 13 | .slice(0, 2) 14 | .join("\n") 15 | 16 | const youch = new Youch(error, req) 17 | 18 | youch.addLink((error) => { 19 | return ` 20 | 21 |
22 | ` 23 | }) 24 | 25 | let missing 26 | 27 | if (error.code === "MODULE_NOT_FOUND") { 28 | missing = error.message.match(/'(.*)'/)[1] 29 | } 30 | 31 | if (error.message.includes("TS2307")) { 32 | missing = error.message.match(/TS2307: Cannot find module '(.*?)'/)[1] 33 | } 34 | 35 | if (error.message.includes("TS7016")) { 36 | missing = error.message.match( 37 | /TS7016: Could not find a declaration file for module '(.*?)'/ 38 | )[1] 39 | 40 | // Rename @feathersjs/express to @types/feathersjs__express 41 | if (missing.startsWith("@")) { 42 | missing = missing.slice(1).replace("/", "__") 43 | } 44 | 45 | missing = `@types/${missing}` 46 | } 47 | 48 | if (missing && !missing.startsWith(".")) { 49 | missing = missing 50 | .split("/") 51 | .slice(0, missing.startsWith("@") ? 2 : 1) 52 | .join("/") 53 | 54 | youch.addLink( 55 | () => ` 56 |
57 | 58 | 59 | 60 | 61 |

62 | Would you like to install ${missing}? 63 |

64 | 65 | 66 | 67 | 68 |
69 | 70 | 73 | 74 | 78 |
79 | ` 80 | ) 81 | } 82 | 83 | youch.addLink(({ message }) => { 84 | const url = `https://google.com/search?q=${encodeURIComponent(message)}` 85 | 86 | return `` 87 | }) 88 | 89 | youch.addLink(({ message }) => { 90 | const url = `https://stackoverflow.com/search?q=${encodeURIComponent( 91 | message 92 | )}` 93 | 94 | return `` 95 | }) 96 | 97 | youch.toHTML().then((html) => { 98 | res.status(statusCode).send(html) 99 | }) 100 | 101 | youch 102 | .toJSON() 103 | .then(forTerminal) 104 | .then(console.log) 105 | } 106 | -------------------------------------------------------------------------------- /src/middleware/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | assets: require("./assets"), 3 | error: require("./error"), 4 | notFound: require("./notFound"), 5 | router: require("./router") 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware/notFound/index.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process") 2 | const { stripIndent } = require("common-tags") 3 | const express = require("express") 4 | const jetpack = require("fs-jetpack") 5 | const opn = require("opn") 6 | const path = require("path") 7 | const generateId = require("uuid/v1") 8 | const waitOn = require("wait-on") 9 | 10 | const nonce = generateId() 11 | 12 | // Default the extension based on what's supporteds 13 | const extension = [".tsx", ".ts", ".jsx", ".js"] 14 | .filter((ext) => require.extensions[ext]) 15 | .shift() 16 | 17 | module.exports = express() 18 | // This handler only responds to GET/POST, not HEAD/OPTIONS/etc. 19 | .use( 20 | function onlyGetPost(req, res, next) { 21 | if (["GET", "POST"].includes(req.method)) { 22 | return next() 23 | } 24 | 25 | return next("route") 26 | }, 27 | function getNotFound(req, res, next) { 28 | if (req.method !== "GET") { 29 | return next() 30 | } 31 | 32 | const filepath = path 33 | .join(process.cwd(), "routes", req.path, `index${extension}`) 34 | .replace(process.cwd(), ".") 35 | 36 | res.status(404).send(` 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 |
47 |

48 | 404 Not Found 49 |

50 | 51 |

52 | ${req.path} 53 |

54 | 55 |
56 | 57 | 58 | 59 | 60 |
61 |
62 | 63 | 66 |
67 | 68 | `) 69 | }, 70 | async function postCreateRoute(req, res, next) { 71 | if (req.method !== "POST") { 72 | return next() 73 | } 74 | 75 | if (req.body.nonce !== nonce) { 76 | throw new Error(`Invalid "nonce" value`) 77 | } 78 | 79 | if (req.body.path !== req.path) { 80 | throw new Error( 81 | `Expected ${JSON.stringify(req.path)}, not ${JSON.stringify( 82 | req.body.path 83 | )}` 84 | ) 85 | } 86 | 87 | const filepath = path.join( 88 | process.cwd(), 89 | "routes", 90 | req.path, 91 | `index${extension}` 92 | ) 93 | 94 | if (jetpack.exists(filepath)) { 95 | throw new Error(`Route already exists at ${filepath}`) 96 | } 97 | 98 | const content = stripIndent( 99 | extension.startsWith(".ts") 100 | ? ` 101 | import { Request, Response } from "express" 102 | 103 | export default (req: Request, res: Response) => { 104 | res.send("📝 ${req.path}") 105 | } 106 | ` 107 | : ` 108 | module.exports = (req, res) => { 109 | res.send("📝 ${req.path}") 110 | } 111 | ` 112 | ) 113 | 114 | // Create the file exists 115 | jetpack.file(filepath, { content }) 116 | 117 | // Wait for the file to exist 118 | await waitOn({ resources: [filepath] }, undefined) 119 | 120 | // Wait for the file to open 121 | execSync(`code . -g ${filepath}`) 122 | 123 | // Reload the requested URL 124 | // ! Hopefully the router has been re-created by this point! 125 | res.redirect(req.originalUrl) 126 | } 127 | ) 128 | -------------------------------------------------------------------------------- /src/middleware/router/bridge.js: -------------------------------------------------------------------------------- 1 | const { request } = require("http") 2 | 3 | module.exports = (port = process.env.PORT) => { 4 | if (!port) { 5 | throw new Error(`Cannot bridge connections without an explicit PORt`) 6 | } 7 | 8 | return function bridge(event) { 9 | const { body, headers, method, path, uuid } = event 10 | const options = { 11 | headers, 12 | method, 13 | port, 14 | path 15 | } 16 | 17 | const req = request(options, (res) => { 18 | const chunks = [] 19 | 20 | process.send({ 21 | headers: res.headers, 22 | statusCode: res.statusCode, 23 | uuid 24 | }) 25 | 26 | res.on("data", (chunk) => { 27 | process.send({ 28 | body: chunk.toString("base64"), 29 | encoding: "base64", 30 | event: "data", 31 | headers: res.headers, 32 | statusCode: res.statusCode, 33 | uuid 34 | }) 35 | 36 | chunks.push(Buffer.from(chunk)) 37 | }) 38 | 39 | res.on("error", (error) => { 40 | throw error 41 | }) 42 | 43 | res.on("end", () => { 44 | delete res.headers.connection 45 | delete res.headers["content-length"] 46 | 47 | process.send({ 48 | body: Buffer.concat(chunks).toString("base64"), 49 | encoding: "base64", 50 | event: "end", 51 | headers: res.headers, 52 | statusCode: res.statusCode, 53 | uuid 54 | }) 55 | }) 56 | }) 57 | 58 | if (body) { 59 | req.write(body) 60 | } 61 | 62 | req.end() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/middleware/router/createRouterFromFiles.js: -------------------------------------------------------------------------------- 1 | const express = require("express") 2 | const path = require("path") 3 | 4 | const handle = require("./handle") 5 | 6 | // Match index[.*|get|post].js 7 | const REGEXP_INDEX = /^index(?:\.(\*|get|post))?\.(?:j|t)sx?$/ 8 | const REGEXP_PARAM = /\[([a-zA-Z0-9-]+)\]/g 9 | const REGEXP_PARAM_REPLACE = ":$1" 10 | const REGEXP_TRAILING_SLASH = /\/+$/ 11 | 12 | module.exports = function createRouterFromFiles(routesPath, files) { 13 | const router = express() 14 | 15 | // Ensure that explict route matches win 16 | files = files 17 | // Ignore unknown route formats 18 | .filter((file) => path.basename(file).match(REGEXP_INDEX)) 19 | .sort((a, b) => { 20 | // Ignore filename when sorting 21 | return path.basename(a) < path.basename(b) ? -1 : 0 22 | }) 23 | // Most specific routes win 24 | .reverse() 25 | 26 | files.forEach((file) => { 27 | const { base, dir } = path.parse(file) 28 | 29 | const route = "/" 30 | // Add relative path to page 31 | .concat(path.relative(routesPath, dir)) 32 | // Convert _param to :param 33 | .replace(REGEXP_PARAM, REGEXP_PARAM_REPLACE) 34 | // Remove trailing slashes (besides root "/") 35 | .replace(REGEXP_TRAILING_SLASH, (match, offset) => 36 | offset > 0 ? "" : match 37 | ) 38 | 39 | const [, method] = base.match(REGEXP_INDEX) 40 | 41 | switch (method) { 42 | case "*": 43 | handle(router, file, [ 44 | ["GET", route], 45 | ["GET", path.join(route, "*")], 46 | ["POST", route], 47 | ["POST", path.join(route, "*")] 48 | ]) 49 | break 50 | 51 | case "post": 52 | handle(router, file, [["POST", route]]) 53 | break 54 | 55 | case "get": 56 | handle(router, file, [["GET", route]]) 57 | break 58 | 59 | case undefined: 60 | handle(router, file, [["GET", route], ["POST", route]]) 61 | break 62 | 63 | default: 64 | throw new Error(`Unsupported route filename: ${file}`) 65 | } 66 | }) 67 | 68 | return router 69 | } 70 | -------------------------------------------------------------------------------- /src/middleware/router/findAvailablePort.js: -------------------------------------------------------------------------------- 1 | const { Server } = require("http") 2 | 3 | module.exports = async function findAvailablePort(port = 4000) { 4 | return new Promise((resolve, reject) => { 5 | const server = new Server() 6 | 7 | server.on("error", (err) => { 8 | if (err.code !== "EADDRINUSE") { 9 | return reject(err) 10 | } 11 | 12 | server.listen(++port) 13 | }) 14 | 15 | server.on("listening", () => { 16 | server.close(() => resolve(port)) 17 | }) 18 | 19 | server.listen(port) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/middleware/router/handle.development.js: -------------------------------------------------------------------------------- 1 | const { fork } = require("child_process") 2 | const debug = require("debug")("polydev") 3 | const path = require("path") 4 | const generateId = require("uuid/v1") 5 | const waitOn = require("wait-on") 6 | const waitPort = require("wait-port") 7 | 8 | const findAvailablePort = require("./findAvailablePort") 9 | 10 | const handlers = new Map() 11 | const launcherPath = path.resolve(__dirname, "./launcher.js") 12 | const responses = new Map() 13 | const cwd = process.cwd() 14 | 15 | module.exports = function handle(router, file, routes) { 16 | const handler = async (req, res, next) => { 17 | const env = { 18 | // Default to "development" 19 | NODE_ENV: "development", 20 | // Favor explicit env values 21 | ...process.env, 22 | // Override PORT since it's dynamic 23 | PORT: await findAvailablePort() 24 | } 25 | 26 | let child = handlers.get(file) 27 | 28 | if (!child || !child.connected) { 29 | child = fork(launcherPath, [file, JSON.stringify(routes)], { cwd, env }) 30 | handlers.set(file, child) 31 | 32 | // Some things have a build step like Next and aren't ready yet. 33 | // TODO This takes ~1-1.5s every time, but I don't know why. 34 | // This can be removed & work for most examples _except_ next. 35 | await waitPort({ interval: 50, output: "silent", port: env.PORT }) 36 | 37 | child.on("message", (message) => { 38 | if (message === "restart") { 39 | handlers.get(file).kill() 40 | return handlers.delete(file) 41 | } 42 | 43 | const { 44 | body, 45 | encoding = "utf8", 46 | headers = {}, 47 | event, 48 | statusCode = 200, 49 | uuid 50 | } = message 51 | 52 | if (!uuid) { 53 | throw new Error( 54 | `Handlers must respond with the corresponding request's UUID` 55 | ) 56 | } 57 | 58 | const response = responses.get(message.uuid) 59 | 60 | if (!response) { 61 | throw new Error(`No response exists for UUID "${uuid}"`) 62 | } 63 | 64 | switch (event) { 65 | case "data": 66 | response.write(Buffer.from(body, encoding)) 67 | break 68 | 69 | case "end": 70 | if (!response.headersSent) { 71 | response.set(headers) 72 | } 73 | 74 | response.status(statusCode) 75 | response.send() 76 | responses.delete(uuid) 77 | 78 | // Server error: restart for next request 79 | if (statusCode === 500) { 80 | handlers.get(file).kill() 81 | handlers.delete(file) 82 | } 83 | 84 | break 85 | 86 | default: 87 | response.set(headers) 88 | response.status(statusCode) 89 | } 90 | }) 91 | } 92 | 93 | const event = { 94 | body: req.rawBody, 95 | headers: req.headers, 96 | host: req.headers.host, 97 | method: req.method, 98 | path: req.url, 99 | uuid: generateId() 100 | } 101 | 102 | responses.set(event.uuid, res) 103 | child.send(event) 104 | } 105 | 106 | routes.forEach(([httpMethod, route]) => { 107 | const method = httpMethod.toLowerCase() 108 | 109 | debug(`router.${method}(%o, %o)`, route, file.replace(process.cwd(), ".")) 110 | 111 | router[method].call( 112 | router, 113 | route, 114 | // Make sure we always evaluate at run-time for the latest HMR'd handler 115 | (req, res, next) => { 116 | const handled = handler(req, res, next) 117 | 118 | // Automatically bubble up async errors 119 | if (handled && handled.catch) { 120 | handled.catch(next) 121 | } 122 | } 123 | ) 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /src/middleware/router/handle.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV = "development" } = process.env 2 | 3 | module.exports = 4 | NODE_ENV === "development" 5 | ? require("./handle.development") 6 | : require("./handle.production") 7 | -------------------------------------------------------------------------------- /src/middleware/router/handle.production.js: -------------------------------------------------------------------------------- 1 | const debug = require("debug")("polydev") 2 | 3 | // ! Jest has a built-in mocking mechanism that can't correctly resolve project 4 | // ! files from node_modules: 5 | // @see https://github.com/facebook/jest/blob/72d01cc79f3dfe05419cd8dea1b6c9a558d93879/packages/jest-resolve/src/index.ts#L277-L279 6 | // 7 | // @ts-ignore 8 | if (require.requireActual) require = require.requireActual 9 | 10 | module.exports = async function handle(router, file, routes) { 11 | await Promise.all( 12 | routes.map(async ([httpMethod, route]) => { 13 | const method = httpMethod.toLowerCase() 14 | const exported = require(file) 15 | 16 | if (!exported) { 17 | return debug( 18 | `Route %o does not have an exported handler from %o`, 19 | route, 20 | file.replace(process.cwd(), ".") 21 | ) 22 | } 23 | 24 | const handler = await (exported.default || exported) 25 | 26 | debug(`router.${method}(%o, %o)`, route, file.replace(process.cwd(), ".")) 27 | 28 | router[method].call( 29 | router, 30 | route, 31 | // Make sure we always evaluate at run-time for the latest HMR'd handler 32 | (req, res, next) => { 33 | const handled = handler(req, res, next) 34 | 35 | // Automatically bubble up async errors 36 | if (handled && handled.catch) { 37 | handled.catch(next) 38 | } 39 | } 40 | ) 41 | }) 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/middleware/router/index.development.js: -------------------------------------------------------------------------------- 1 | const chokidar = require("chokidar") 2 | const express = require("express") 3 | const path = require("path") 4 | 5 | const createRouterFromFiles = require("./createRouterFromFiles") 6 | 7 | module.exports = function createRouter(routesPath = "routes") { 8 | routesPath = path.resolve(routesPath) 9 | 10 | // Start with a blank router before routes get loaded 11 | let router = express() 12 | 13 | const watcher = chokidar.watch(routesPath, { ignoreInitial: true }) 14 | 15 | const updateRouter = () => { 16 | const watched = watcher.getWatched() 17 | const folders = Object.keys(watched) 18 | 19 | // Convert { [folder]: [...base] } to [...filepaths] 20 | const files = folders.reduce((acc, folder) => { 21 | return [ 22 | ...acc, 23 | ...watched[folder].map((base) => path.resolve(folder, base)) 24 | ] 25 | }, []) 26 | 27 | router = createRouterFromFiles(routesPath, files) 28 | } 29 | 30 | watcher 31 | .on("add", updateRouter) 32 | .on("ready", updateRouter) 33 | .on("unlink", updateRouter) 34 | 35 | // Ensure each request references the latest router 36 | return (req, res, next) => router(req, res, next) 37 | } 38 | -------------------------------------------------------------------------------- /src/middleware/router/index.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV = "development" } = process.env 2 | 3 | module.exports = 4 | NODE_ENV === "development" 5 | ? require("./index.development") 6 | : require("./index.production") 7 | -------------------------------------------------------------------------------- /src/middleware/router/index.production.js: -------------------------------------------------------------------------------- 1 | const express = require("express") 2 | const jetpack = require("fs-jetpack") 3 | const path = require("path") 4 | 5 | const createRouterFromFiles = require("./createRouterFromFiles") 6 | 7 | module.exports = function createRouter(routesPath = "routes") { 8 | routesPath = path.resolve(routesPath) 9 | 10 | const files = jetpack 11 | .find(routesPath, { matching: "*", recursive: true }) 12 | .map((file) => path.resolve(file)) 13 | 14 | return createRouterFromFiles(routesPath, files) 15 | } 16 | -------------------------------------------------------------------------------- /src/middleware/router/launcher.js: -------------------------------------------------------------------------------- 1 | // TODO HMR doesn't work when replacing the entire server. 2 | // How can we make that more resilient? Mock out `.listen` to work _once_? 3 | // Disable HMR for the entry file only? 4 | // 5 | // Or, recommend people use `module.hot`: 6 | // > https://github.com/sidorares/hot-module-replacement/blob/master/examples/express-hot-routes/server.js 7 | require("hot-module-replacement")({ 8 | // options are optional 9 | ignore: /node_modules/ // regexp to decide if module should be ignored; also can be a function accepting string and returning true/false 10 | }) 11 | 12 | const { spawn } = require("child_process") 13 | const express = require("express") 14 | const path = require("path") 15 | 16 | const bridge = require("./bridge") 17 | 18 | const { PORT } = process.env 19 | const [, , handlerPath, routesString] = process.argv 20 | 21 | // Expected to be JSON.stringify([["GET", "/"]]) 22 | const routes = JSON.parse(routesString) 23 | 24 | const verify = (req, res, buffer, encoding = "utf8") => { 25 | if (buffer && buffer.length) { 26 | req.rawBody = buffer.toString(encoding) 27 | } 28 | } 29 | 30 | // TODO Remove baseUrl unless it's needed in the route 31 | async function startHandler() { 32 | const getLatestHandler = async () => { 33 | const exported = require(handlerPath) 34 | const handler = exported ? await (exported.default || exported) : exported 35 | 36 | return handler 37 | } 38 | 39 | // Next.js returns a Promise for when the server is ready 40 | let handler = await getLatestHandler().catch((error) => { 41 | return function invalidHandler(req, res, next) { 42 | next(error) 43 | } 44 | }) 45 | 46 | // @ts-ignore 47 | if (module.hot) { 48 | let recentlySaved = false 49 | 50 | // @ts-ignore 51 | module.hot.accept(handlerPath, async () => { 52 | // Best way to ensure that HMR doesn't save old copies 53 | delete require.cache[handlerPath] 54 | 55 | if (recentlySaved) { 56 | console.log(`♻️ Restarting ${handlerPath}`) 57 | return process.send("restart") 58 | } 59 | 60 | handler = await getLatestHandler() 61 | console.log(`🔁 Hot-reloaded ${handlerPath}`) 62 | 63 | // TODO Send reload signal 64 | 65 | // Wait for a double-save 66 | recentlySaved = true 67 | // Outside of double-save reload window 68 | setTimeout(() => { 69 | recentlySaved = false 70 | }, 500) 71 | }) 72 | } 73 | 74 | const url = `http://localhost:${PORT}/` 75 | 76 | if (typeof handler === "function") { 77 | const app = express() 78 | 79 | // req.body is needed 80 | app.use(express.urlencoded({ extended: true, verify })) 81 | app.use(express.json({ verify })) 82 | 83 | routes.forEach(([method, route]) => { 84 | app[method.toLowerCase()].call( 85 | app, 86 | route, 87 | // Make sure we always evaluate at run-time for the latest HMR'd handler 88 | function handleRoute(req, res, next) { 89 | getLatestHandler() 90 | .then((handler) => { 91 | const handled = handler(req, res, next) 92 | 93 | // Automatically bubble up async errors 94 | if (handled && handled.catch) { 95 | handled.catch(next) 96 | } 97 | }) 98 | .catch(next) 99 | } 100 | ) 101 | }) 102 | 103 | // When there's an uncaught error in the middleware, send it in a way 104 | // that we can handle. 105 | app.use(require("../error")) 106 | 107 | app.listen(PORT, async () => { 108 | console.log(`↩︎ ${handlerPath.replace(process.cwd(), ".")} from ${url}`) 109 | }) 110 | } else if (typeof handler === "string") { 111 | // Expected to have path to `package.json` 112 | const pkg = require(handler) 113 | const cwd = path.dirname(handler) 114 | 115 | spawn("yarn", [pkg.scripts.dev ? "dev" : "start"], { 116 | cwd, 117 | stdio: "inherit" 118 | }) 119 | } else { 120 | console.warn( 121 | `${handlerPath.replace( 122 | process.cwd(), 123 | "." 124 | )} does not return a Function, Server, or path to a package.json` 125 | ) 126 | // In development, at least listen on PORT so that we can 404 127 | express().listen(PORT) 128 | } 129 | 130 | process.on("message", bridge(PORT)) 131 | } 132 | 133 | startHandler() 134 | -------------------------------------------------------------------------------- /src/public/styles.css: -------------------------------------------------------------------------------- 1 | @keyframes fadeIn { 2 | from { 3 | opacity: 0; 4 | } 5 | 6 | to { 7 | opacity: 1; 8 | } 9 | } 10 | 11 | @keyframes rotate { 12 | from { 13 | filter: hue-rotate(0deg); 14 | } 15 | 16 | to { 17 | filter: hue-rotate(360deg); 18 | } 19 | } 20 | 21 | body, 22 | html { 23 | font-family: 'Quicksand', sans-serif; 24 | font-size: 16px; 25 | font-weight: 300; 26 | height: 100%; 27 | margin: 0; 28 | width: 100%; 29 | } 30 | 31 | body { 32 | align-items: center; 33 | animation: fadeIn 1s; 34 | background: white; 35 | display: flex; 36 | justify-content: center; 37 | } 38 | 39 | form { 40 | margin: 0; 41 | } 42 | 43 | #splash { 44 | animation: rotate 15s alternate; 45 | background: url("./triangilify.svg") no-repeat center center fixed; 46 | background-size: cover; 47 | bottom: 0; 48 | left: 0; 49 | position: fixed; 50 | right: 0; 51 | top: 0; 52 | z-index: -1; 53 | } 54 | 55 | h1, 56 | h2, 57 | h3, 58 | h4, 59 | h5, 60 | h6 { 61 | font-weight: 500; 62 | } 63 | 64 | h2.error-message { 65 | text-shadow: 0 1px 0px white 66 | } 67 | 68 | section:not([class]) { 69 | background: white; 70 | border-radius: 3px; 71 | box-shadow: 0 2vw 4vw 0 rgba(0, 0, 0, 0.11), 0 2vw 4vw 0 rgba(0, 0, 0, 0.08); 72 | max-height: 80vh; 73 | max-width: 80vw; 74 | overflow: auto; 75 | } 76 | 77 | section.error-page .fab, 78 | button { 79 | background: rgb(250, 250, 250); 80 | border: 1px solid white; 81 | border-radius: 100em; 82 | box-shadow: 0 0.1rem 0.3rem rgba(0, 0, 0, 0.2); 83 | color: #455275; 84 | cursor: pointer; 85 | margin-right: 0.5rem; 86 | padding: 0.5rem 1rem; 87 | transition: all 200ms; 88 | } 89 | 90 | section.error-page .fab:hover, 91 | section.error-page button:hover { 92 | background: white; 93 | border-color: #455275; 94 | box-shadow: 0 0.1rem 0.3rem rgba(0, 0, 0, 0.3); 95 | } 96 | 97 | section.error-page form { 98 | background: rgb(250, 250, 250, 0.5); 99 | border-radius: 3px; 100 | margin-bottom: 2rem; 101 | padding: 1rem; 102 | } 103 | 104 | section.error-page form h3 { 105 | margin-bottom: 1rem; 106 | } 107 | 108 | section.error-stack { 109 | background: rgba(100%, 100%, 100%, 0.5) 110 | } 111 | 112 | section.request-details { 113 | background: white; 114 | box-shadow: 0 5em 10em black 115 | } 116 | 117 | section header, 118 | section main, 119 | section footer { 120 | overflow: auto; 121 | padding: 2.5ch 5ch; 122 | } 123 | 124 | section main+footer { 125 | background: #f5f5f5; 126 | border-top: 1px solid #eee; 127 | } 128 | 129 | kbd { 130 | background-color: #fafbfc; 131 | border: 1px solid #c6cbd1; 132 | border-bottom-color: rgb(198, 203, 209); 133 | border-bottom-color: #959da5; 134 | border-radius: 3px; 135 | box-shadow: inset 0 -1px 0 #959da5; 136 | color: #444d56; 137 | display: inline-block; 138 | padding: 3px 5px; 139 | vertical-align: middle; 140 | } 141 | 142 | hr { 143 | background: rgba(0, 0, 0, 0.05); 144 | height: 1px; 145 | border: 0; 146 | margin: 1rem 0; 147 | } 148 | 149 | pre { 150 | animation: fadein 2s; 151 | background: #222; 152 | border-radius: 3px; 153 | color: #fff; 154 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif; 155 | font-size: 13px; 156 | height: 25ch; 157 | line-height: 20px; 158 | padding: 1rem; 159 | overflow: auto; 160 | width: 80ch; 161 | } 162 | -------------------------------------------------------------------------------- /src/public/triangilify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | -------------------------------------------------------------------------------- /src/routes/_polydev/install-module/index.js: -------------------------------------------------------------------------------- 1 | const Convert = require("ansi-to-html") 2 | const { spawn } = require("child_process") 3 | 4 | const convert = new Convert({ 5 | fg: "#eee", 6 | bg: "#222", 7 | newline: false, 8 | escapeXML: true, 9 | stream: true 10 | }) 11 | 12 | module.exports = (req, res) => { 13 | if (!req.body.module) { 14 | throw new Error(`Missing module not defined`) 15 | } 16 | 17 | res.writeHead(200, { 18 | "Content-Type": "text/html; charset=utf-8", 19 | "Transfer-Encoding": "chunked", 20 | "X-Content-Type-Options": "nosniff" 21 | }) 22 | 23 | res.write(` 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |

35 | Installing ${req.body.module}… 36 |

37 | 38 |
`)
39 | 
40 |   const args = ["add", req.body.module]
41 | 
42 |   if (req.body.dev) {
43 |     args.push("--dev")
44 |   }
45 | 
46 |   const child = spawn("yarn", args)
47 | 
48 |   res.write(`$ yarn ${args.join(" ")}\n`)
49 | 
50 |   child.stderr.on("data", (data) => res.write(convert.toHtml(`${data}`)))
51 |   child.stdout.on("data", (data) => res.write(convert.toHtml(`${data}`)))
52 | 
53 |   child.on("close", (code, signal) => {
54 |     if (!code) {
55 |       res.write(`
56 |         
57 |       `)
58 |     }
59 | 
60 |     res.end()
61 |   })
62 | }
63 | 


--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
 1 | const express = require("express")
 2 | const opn = require("opn")
 3 | 
 4 | const { polydev } = require(".")
 5 | const { PORT = 3000 } = process.env
 6 | 
 7 | process.on("uncaughtException", (error) => {
 8 |   // TODO Youch
 9 |   console.error("uncaughtException", error)
10 | })
11 | 
12 | process.on("unhandledRejection", (error) => {
13 |   // TODO Youch
14 |   console.error("unhandledRejection", error)
15 | })
16 | 
17 | const proxy = express()
18 | 
19 | proxy.use(polydev())
20 | 
21 | const server = proxy.listen(PORT, () => {
22 |   const url = `http://localhost:${server.address().port}/`
23 | 
24 |   console.log(`🚀 Ready! ${url}`)
25 | 
26 |   if (process.argv.includes("--open")) {
27 |     opn(url)
28 |   }
29 | })
30 | 


--------------------------------------------------------------------------------