├── .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 | 
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 | 
2 |
3 | > Faster, route-centric development for [Node.js][node] apps with built-in
4 | > [Hot Module Replacement][hmr].
5 | >
6 | > 
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------