├── .github └── workflows │ ├── generate_changelog.yml │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo.gif ├── esbuild.js ├── package-lock.json ├── package.json ├── src ├── bin.js ├── index.js ├── lib │ ├── bin.js │ ├── colors.js │ ├── http.js │ ├── livereload.js │ ├── mime.json │ ├── params.json │ ├── table.js │ └── watch.js └── plugins │ └── rollup.js └── test ├── public ├── assets │ └── style.css ├── index.html └── subdir │ └── index.html └── test.js /.github/workflows/generate_changelog.yml: -------------------------------------------------------------------------------- 1 | name: Generate Changelog 2 | 3 | on: 4 | push: 5 | branches: [ main, master] 6 | 7 | jobs: 8 | changelog: 9 | name: Update Changelog 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Update Changelog 17 | uses: AlexxNB/chalogen@master 18 | with: 19 | title: Derver changelog 20 | - name: Commit Changelog to repository 21 | uses: stefanzweifel/git-auto-commit-action@v4 22 | with: 23 | commit_message: 'docs(Changelog): Update Changelog' 24 | file_pattern: CHANGELOG.md -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish on NPM 2 | 3 | on: 4 | push: 5 | paths: 6 | - '.github/workflows/npm-publish.yml' 7 | - 'package.json' 8 | 9 | jobs: 10 | publish-npm: 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Setup Node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 15 18 | registry-url: https://registry.npmjs.org/ 19 | - name: Installing NPM deps 20 | run: npm install 21 | - name: Build modules 22 | run: npm run build 23 | - name: Publishing on NPM 24 | run: npm publish 25 | env: 26 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | dist 4 | *-plugin-cjs.js 5 | *-plugin-esm.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | esbuild.js 3 | test 4 | demo.gif -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Derver changelog 2 | 3 | ## unreleased 4 | 5 | ### Other 6 | 7 | - Merge branch 'main' of github.com:AlexxNB/derver [`6f8d7d97`](https://github.com/AlexxNB/derver/commit/6f8d7d9750ee830e0e08bd2e84d6b0854beef340) 8 | 9 | ## 0.5.3 - 2021-11-11 10 | 11 | ### Other 12 | 13 | - add node path [`1dcd969c`](https://github.com/AlexxNB/derver/commit/1dcd969c3594fddb38ef9033ffc0591ea661b44d) 14 | - Merge branch 'main' of github.com:AlexxNB/derver [`04ddf9fd`](https://github.com/AlexxNB/derver/commit/04ddf9fd2570970f3ea7299ad194ed40c05541fe) 15 | 16 | ## 0.5.2 - 2021-11-06 17 | 18 | ### Features 19 | 20 | - Preserve scroll position on reload [`483cc95a`](https://github.com/AlexxNB/derver/commit/483cc95ac5a9877976c104cd9ef79ba9eb6f3113) 21 | 22 | ### Other 23 | 24 | - Merge branch 'main' of github.com:AlexxNB/derver [`e26d23c7`](https://github.com/AlexxNB/derver/commit/e26d23c79a01ae5cd2b08dace8a406bcd3be5633) 25 | 26 | ## 0.5.1 - 2021-11-01 27 | 28 | ### Bug Fixes 29 | 30 | - 🐛 Add exports for main and module also [`efaccc30`](https://github.com/AlexxNB/derver/commit/efaccc307a062ae5394edebfe46f458771636c76) 31 | 32 | ### Other 33 | 34 | - Merge branch 'main' of github.com:AlexxNB/derver [`6cb79cb4`](https://github.com/AlexxNB/derver/commit/6cb79cb444efa9aba36fce0fcfa20ddebde62040) 35 | 36 | ## 0.4.20 - 2021-10-30 37 | 38 | ### Other 39 | 40 | - make rollup-plugin universal fo cjs and esm consumers [`48f130b6`](https://github.com/AlexxNB/derver/commit/48f130b6a6fbfcdbe7ad5b678afcb7905917f1a0) 41 | - Merge branch 'main' of github.com:AlexxNB/derver into main [`3779af12`](https://github.com/AlexxNB/derver/commit/3779af128dd0a67bf42ec5926834ba2afbc04159) 42 | 43 | ## 0.4.19 - 2021-09-01 44 | 45 | ### Features 46 | 47 | - Add options to prevent console trashing [`6883c207`](https://github.com/AlexxNB/derver/commit/6883c207d93c82638000a73f3696d23e5bb88a44) 48 | 49 | ## 0.4.18 - 2021-09-01 50 | 51 | ### Features 52 | 53 | - Ability to run only middlewares when dir option is false [`1c3c5bc1`](https://github.com/AlexxNB/derver/commit/1c3c5bc1a813369fca2338b9398f01e8e9a48bfb) 54 | 55 | ### Other 56 | 57 | - Merge branch 'main' of github.com:AlexxNB/derver into main [`082778e8`](https://github.com/AlexxNB/derver/commit/082778e8fc04c59de3eaca59dc6bd6ab3475f7bd) 58 | 59 | ## 0.4.17 - 2021-06-28 60 | 61 | ### Features 62 | 63 | - Add JSON handling in middlewares [`f967e600`](https://github.com/AlexxNB/derver/commit/f967e600240397155148ae9a0ca23754ebb7e81c) 64 | 65 | ### Other 66 | 67 | - Merge branch 'main' of github.com:AlexxNB/derver into main [`554a0eb8`](https://github.com/AlexxNB/derver/commit/554a0eb89f1c7407316dfb76016cb943a53de207) 68 | 69 | ## 0.4.16 - 2021-06-12 70 | 71 | ### Documentation 72 | 73 | - 📝 Fix some mistakes [`cbc6d30e`](https://github.com/AlexxNB/derver/commit/cbc6d30eb5f5bc61190df6e8238a4c003ecda927) 74 | 75 | ### Other 76 | 77 | - Merge branch 'main' of github.com:AlexxNB/derver into main [`0b76ccdf`](https://github.com/AlexxNB/derver/commit/0b76ccdfda9bdef2440b089706e860ce8a10ccce) 78 | - update dapendencies [`21174900`](https://github.com/AlexxNB/derver/commit/21174900c30c804640dbd97bc2b07664374e50db) 79 | 80 | ## 0.4.15 - 2021-05-02 81 | 82 | ### Features 83 | 84 | - Allow nested index fallbaks in SPA mode [`16e52070`](https://github.com/AlexxNB/derver/commit/16e52070e992cd252171099aeba12bcf0d419d7d) 85 | 86 | *If there is some sub directory contents index.html then unexistent URL with first part matched this subdirectory will fallback to this index file instead root one.* 87 | 88 | ### Bug Fixes 89 | 90 | - 🐛 Last param from URL contents also a query part [`10c775ec`](https://github.com/AlexxNB/derver/commit/10c775ec9bd4968564d21212b9db77886ec4c11d) 91 | 92 | ## 0.4.14 - 2021-03-20 93 | 94 | ### Bug Fixes 95 | 96 | - 🐛 Livereload doesn't work when no watchers, but uses remote. ([#1](https://github.com/AlexxNB/derver/issues/1)) [`dc79b251`](https://github.com/AlexxNB/derver/commit/dc79b251f1d518dc6c0a22a25cdae558f2e6ed84) 97 | 98 | *fix [#1](https://github.com/AlexxNB/derver/issues/1)* 99 | 100 | ## 0.4.13 - 2021-03-10 101 | 102 | ### Other 103 | 104 | - fix 404 with query add usful data to request object [`226450e5`](https://github.com/AlexxNB/derver/commit/226450e5460c3db28951a2b38622a1f46aaf8c16) 105 | 106 | ## 0.4.12 - 2021-01-22 107 | 108 | ### Other 109 | 110 | - dartheme for error modal [`a3e6214f`](https://github.com/AlexxNB/derver/commit/a3e6214fa548fa4ce23e3f26c42265a055605df3) 111 | 112 | ## 0.4.11 - 2021-01-21 113 | 114 | ### Other 115 | 116 | - show modal when server down [`0c780542`](https://github.com/AlexxNB/derver/commit/0c7805429bf4e12cdcc9e5cdd7f01de14caea92e) 117 | 118 | ## 0.4.10 - 2021-01-21 119 | 120 | ### Other 121 | 122 | - get remote config in each request [`fa781f69`](https://github.com/AlexxNB/derver/commit/fa781f69d81089f4b482f141e6668c16e4830ea5) 123 | 124 | ## 0.4.9 - 2021-01-21 125 | 126 | ### Other 127 | 128 | - add remote server ID option [`809f22df`](https://github.com/AlexxNB/derver/commit/809f22dfd920e36767a8225fc7108b510abad760) 129 | 130 | ## 0.4.8 - 2021-01-21 131 | 132 | ### Other 133 | 134 | - add remote control [`a815b00b`](https://github.com/AlexxNB/derver/commit/a815b00be5c735d3944047719fa3eeacafa0d2be) 135 | 136 | ## 0.4.7 - 2021-01-16 137 | 138 | ### Other 139 | 140 | - fix nesting [`9a79fc26`](https://github.com/AlexxNB/derver/commit/9a79fc2644f868f6a7b7a6b7ea06d8613e356500) 141 | 142 | ## 0.4.6 - 2021-01-16 143 | 144 | ### Other 145 | 146 | - add nested middlewares [`ab208b25`](https://github.com/AlexxNB/derver/commit/ab208b256f0f3fac59228ce03f30516af731e9bf) 147 | 148 | ## 0.4.5 - 2021-01-13 149 | 150 | ### Other 151 | 152 | - fix middlewares wrong order [`fcbea885`](https://github.com/AlexxNB/derver/commit/fcbea885d696956f1c88cbd37d1e8f3c70374733) 153 | 154 | ## 0.4.4 - 2021-01-13 155 | 156 | ### Other 157 | 158 | - fix chaining [`7b16b249`](https://github.com/AlexxNB/derver/commit/7b16b2498e469bab07c382910e773735c3cd6bd9) 159 | 160 | ## 0.4.3 - 2021-01-12 161 | 162 | ### Other 163 | 164 | - debounce watch log [`b79943de`](https://github.com/AlexxNB/derver/commit/b79943deb4939be8ac2c6bdac7c3b686f48db6bc) 165 | 166 | ## 0.4.2 - 2021-01-12 167 | 168 | ### Other 169 | 170 | - close watchers on exit [`701aa4e9`](https://github.com/AlexxNB/derver/commit/701aa4e9623a1ba43c4ba9d74a1bebadf91c2afa) 171 | 172 | ## 0.4.1 - 2021-01-12 173 | 174 | ### Other 175 | 176 | - stop server on process exit [`91802948`](https://github.com/AlexxNB/derver/commit/9180294806ff3e8c352c403ab005aa6cc3676471) 177 | 178 | ## 0.4.0 - 2021-01-12 179 | 180 | ### Other 181 | 182 | - add middlewares [`d80449e6`](https://github.com/AlexxNB/derver/commit/d80449e6387ae79f4e648e0b8015de859edbaad0) 183 | 184 | ## 0.3.0 - 2020-11-18 185 | 186 | ### Other 187 | 188 | - fix livereload url [`4b2179df`](https://github.com/AlexxNB/derver/commit/4b2179df512959e03d2c63b518bb99e6380bfdb6) 189 | - use node-watch instead fs.watch [`fb4260b7`](https://github.com/AlexxNB/derver/commit/fb4260b780c5f74c22b924f4d76a31f779e467cf) 190 | 191 | ## 0.2.2 - 2020-11-14 192 | 193 | ### Other 194 | 195 | - add rollup plugin [`9617f935`](https://github.com/AlexxNB/derver/commit/9617f935eb6dbbf1f82938929e134768eb26b8fe) 196 | 197 | ## 0.2.1 - 2020-11-07 198 | 199 | ### Other 200 | 201 | - fix livereload [`c9206ceb`](https://github.com/AlexxNB/derver/commit/c9206ceb803e50c21afaca6c1b8c3424a9a5fdd9) 202 | 203 | ## 0.2.0 - 2020-11-06 204 | 205 | ### Other 206 | 207 | - fix dist size [`384563d5`](https://github.com/AlexxNB/derver/commit/384563d53b4b326db17d267199da2168d88751f5) 208 | - add production mode [`54053370`](https://github.com/AlexxNB/derver/commit/540533701fc53fdd7eeb6c2a96e5194fece808da) 209 | - add server header [`595bd82f`](https://github.com/AlexxNB/derver/commit/595bd82fd9159b791f2687bd317147a7b2af449b) 210 | - add spa mode [`a4b8d98b`](https://github.com/AlexxNB/derver/commit/a4b8d98b0485e4fab7332285abe1616731fa541a) 211 | - add cache control [`257d06cc`](https://github.com/AlexxNB/derver/commit/257d06cc8ed4955d940cebc6a0a45a86a743a9ae) 212 | - add compression support [`0c727c3d`](https://github.com/AlexxNB/derver/commit/0c727c3d436d44d422ec52df4fe7b199ca9a0f58) 213 | - refactor middlewares [`1d1c893d`](https://github.com/AlexxNB/derver/commit/1d1c893dc4ccf3fb320f9ae97a3f1d6b1e101658) 214 | 215 | ## 0.1.6 - 2020-11-05 216 | 217 | ### Other 218 | 219 | - fix URL [`b6563bb2`](https://github.com/AlexxNB/derver/commit/b6563bb27d856233c8705f3b86c49b96996daa64) 220 | 221 | ## 0.1.5 - 2020-11-05 222 | 223 | ### Other 224 | 225 | - add gif [`a2ecbc68`](https://github.com/AlexxNB/derver/commit/a2ecbc68168fa9aad603ceda9e8b581b2bc4ab63) 226 | 227 | ## 0.1.4 - 2020-11-05 228 | 229 | ### Other 230 | 231 | - fix readme [`eea9c9a0`](https://github.com/AlexxNB/derver/commit/eea9c9a0e24cb3489d914e65e18c6e3032840e7b) 232 | - add error modal [`1a76777d`](https://github.com/AlexxNB/derver/commit/1a76777de9d163b00dafcb432962fb1a33163659) 233 | 234 | ## 0.1.3 - 2020-11-03 235 | 236 | ### Other 237 | 238 | - add livereload.console method [`dd9b5d05`](https://github.com/AlexxNB/derver/commit/dd9b5d05a120f897bbd05d876a8e9dd7f463cace) 239 | 240 | ## 0.1.2 - 2020-11-03 241 | 242 | ### Other 243 | 244 | - fix bin path [`3373db20`](https://github.com/AlexxNB/derver/commit/3373db202f34f5396171edd5cdcb00d151c03d96) 245 | 246 | ## 0.1.1 - 2020-11-03 247 | 248 | ### Other 249 | 250 | - fix repo [`b8415e5e`](https://github.com/AlexxNB/derver/commit/b8415e5e3a70963a65dc7c5a138cbe20d9c916ab) 251 | - fix formatting [`e38ff2e4`](https://github.com/AlexxNB/derver/commit/e38ff2e479710bb7b44af108e85fa7e4cb1d5bb3) 252 | - add publish workflow [`12e75eee`](https://github.com/AlexxNB/derver/commit/12e75eeeaf7bc6199648e94d725031ecdb0e2b4a) 253 | - fix [`a8cdb039`](https://github.com/AlexxNB/derver/commit/a8cdb0398a317f5a5980a755691974a1fa59a89d) 254 | - add readme [`43e5601c`](https://github.com/AlexxNB/derver/commit/43e5601cd49e1867658aa80e0b3dec9e70992201) 255 | - add bin [`8906e54b`](https://github.com/AlexxNB/derver/commit/8906e54b6e81bd76602480e9ee6dba383273f7c8) 256 | - autoreconnection for livereload [`77274ee1`](https://github.com/AlexxNB/derver/commit/77274ee16f4db4e19204663fed03dfa2fa515b55) 257 | - make onwatch asyncable [`0748ba69`](https://github.com/AlexxNB/derver/commit/0748ba697b4cfb25d80a2b951afa70504e9a896c) 258 | - add onwatch property [`e339482a`](https://github.com/AlexxNB/derver/commit/e339482a92c282ca44a71466f89e74bfee03ede4) 259 | - more beutiful output [`fe069db4`](https://github.com/AlexxNB/derver/commit/fe069db42e7bf9a75c19ea3b6715dfc1fba9857c) 260 | - colorful output [`bd123a2a`](https://github.com/AlexxNB/derver/commit/bd123a2a7ece834d37f58784df348c2df9936999) 261 | - made liverload [`77bbbb2f`](https://github.com/AlexxNB/derver/commit/77bbbb2f238765f359e222f1b892dc8edb8643cb) 262 | - add js injector [`c2105994`](https://github.com/AlexxNB/derver/commit/c21059946193bfe6dbaaaa200c8746e914453b2e) 263 | - rearrange config [`25c4192f`](https://github.com/AlexxNB/derver/commit/25c4192ff318d1d286de546dd2b6950eefbf9517) 264 | - make lib [`af79434a`](https://github.com/AlexxNB/derver/commit/af79434a1e5a6b1fa5e83cce5a40924988f8564e) 265 | - add debouncer [`8e0827c4`](https://github.com/AlexxNB/derver/commit/8e0827c4029a506ba39828eb05918acbfd6d43d4) 266 | - add watcher [`61de3da7`](https://github.com/AlexxNB/derver/commit/61de3da7e64d19d0dacf7589c38c339db13cb6a8) 267 | - initial commit [`1efced54`](https://github.com/AlexxNB/derver/commit/1efced541bc5948407d41f0473b3747cd956a396) 268 | - Initial commit [`5b9824df`](https://github.com/AlexxNB/derver/commit/5b9824df0e5e9d00b26bfc291e6270561f09fe67) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexey Schebelev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Derver 2 | 3 | Tiny Development Server for your web-applications with livereload and watchers 4 | 5 | 6 |

7 | 8 |

9 | 10 | ## Features 11 | 12 | * Very tiny (~8kb) 13 | * Livereload 14 | * Watch directories with callback 15 | * CLI or JS API 16 | * Supports Gzip or Brotli compression 17 | * Cache control 18 | * Supports SPA mode 19 | 20 | ## CLI Usage 21 | 22 | ```sh 23 | derver [parameters] [serve_directory] 24 | ``` 25 | 26 | You may use `npx derver` instantly or install globally by `npm install -g derver` and then use command `derver`. 27 | 28 | To serve and watching current directory just run: 29 | 30 | ```sh 31 | derver 32 | ``` 33 | 34 | Server will run on [http://localhost:7000]() and site will be reloaded each time you change the file in the served directory. 35 | 36 | By default server is running on `localhost` and nobody can access on the website from the network. Bind server on the `0.0.0.0` interface to allow network connections: 37 | 38 | ```sh 39 | derver --host=0.0.0.0 public 40 | ``` 41 | 42 | In this example, `public` is a directory where are files for serving. By default it is current directory. 43 | 44 | ### List of possible parameters 45 | 46 | #### `--host` 47 | Interface, where bind the server. Use `0.0.0.0` inside Docker container or when need network connections to your site. 48 | *Default: localhost* 49 | *Example: --host=localhost* 50 | 51 | #### `--port` 52 | Port, where bind the server. 53 | *Default: 7000* 54 | *Example: --port=8080* 55 | 56 | #### `--index` 57 | Name of the root file of web directory. Webserver will lookup this file when no file specified in the requested URL. 58 | *Default: index.html* 59 | *Example: --index=index.htm* 60 | 61 | #### `--watch` 62 | Specify the directories for watching files changes. Each time when files modified in these directories, website will be reloaded. You may use this parameter multiple times to set more than one directory for watching. 63 | *Default: watch the served directory* 64 | *Example: --watch=dist/public --watch=src* 65 | 66 | #### `--no-watch` 67 | Add this parameter when you want to disable any watching and livereloading. 68 | *Example: --no-watch* 69 | 70 | #### `--compress` 71 | Will return files compressed by gzip or brotli, if client supports it. 72 | *Example: --compress* 73 | 74 | #### `--cache` 75 | Add `Cache-control` header to the response with `max-age` equal `31536000` (~1 year). You can specify number of seconds. 76 | *Example: --cache* 77 | *Example: --cache=3600* 78 | 79 | #### `--scroll` 80 | Restore scroll position on every reload. Number value is equal timeout before scroll restoration. 81 | *Example: --scroll* 82 | *Example: --scroll=100* 83 | 84 | #### `--spa` 85 | Enables SPA (Single-Page Application) mode. All requested pages will be responsed by index page in the application root, which is specified in `--index` parameter. 86 | *Example: --spa* 87 | 88 | #### `--production` 89 | Run server in production mode(really not, use at own risk). It enables `--cache`, `--compress` and `--no-watch` parameters. Also host will set on `0.0.0.0` to handle connections from the network. 90 | 91 | *Example: --production* 92 | 93 | ## Javascript API 94 | 95 | You may use Derver in the your scripts to get more power. 96 | 97 | Install Derver as local dependency: 98 | 99 | ``` 100 | npm install derver 101 | ``` 102 | 103 | Then use `derver` function from the `derver` package to start the server. 104 | 105 | ```js 106 | import {derver} from 'derver'; 107 | 108 | derver(); 109 | ``` 110 | 111 | By default, server will be started on [http://localhost:7000]() and serve `public` directory in your workdir. 112 | 113 | ### Configuration 114 | 115 | You may set configuration object as a parameter of the `derver` function. Below you find all possible options: 116 | 117 | #### `dir` *string*|boolean 118 | Directory which contains files for serving. If nothing set in `watch` option, it will be watching for changes also. When it is `false` - no files would be serving, only middlewares will work. 119 | 120 | 121 | *Default: public* 122 | 123 | 124 | *Example: dir: 'public'* 125 | *Example: dir: false* 126 | 127 | --- 128 | 129 | #### `host` *string* 130 | Interface, where bind the server. Use `0.0.0.0` inside docker or when need network connections to your site. 131 | 132 | 133 | *Default: localhost* 134 | 135 | 136 | *Example: host: 'localhost'* 137 | 138 | --- 139 | 140 | #### `port` *number* 141 | Port, where bind the server. 142 | 143 | 144 | *Default: 7000* 145 | 146 | 147 | *Example: port: 8080* 148 | 149 | --- 150 | 151 | #### `index` *string* 152 | Name of the root file of web directory. Webserver will lookup this file when no file specified in the requested URL. 153 | 154 | *Default: index.html* 155 | 156 | *Example: index: 'index.htm'* 157 | 158 | --- 159 | 160 | #### `compress` *boolean* 161 | Will return files compressed by gzip or brotli, if client supports it. 162 | 163 | 164 | *Default: false* 165 | 166 | 167 | *Example: compress: true* 168 | 169 | --- 170 | 171 | #### `cache` *boolean*|*number* 172 | Add `Cache-control` header to the response with `max-age` equal `31536000` (~1 year). You can specify number of seconds. 173 | 174 | 175 | *Default: false* 176 | 177 | 178 | *Example: cache: true* 179 | 180 | 181 | *Example: cache: 3600* 182 | 183 | --- 184 | 185 | #### `spa` *boolean* 186 | Enables SPA (Single-Page Application) mode. All requested pages will be responced by index page in the application root, which is specified in `index` option. 187 | 188 | 189 | *Default: false* 190 | 191 | 192 | *Example: spa: true* 193 | 194 | --- 195 | 196 | #### `watch` *string*|*array of string* 197 | Specify the directories for watching filechanges. Each time when files modified in theese directories, website will be reloaded and `onwatch` callback will be run. By default will be watched directory defined in `dir` option. 198 | 199 | 200 | *Default: null* 201 | 202 | 203 | *Example: watch: ['dist/public','src']* 204 | 205 | --- 206 | 207 | #### `remote` *boolean*|*string* 208 | Enables remote control listener. See [Remote control](#remote-control) 209 | 210 | 211 | *Default: false* 212 | 213 | 214 | *Example: remote: true* 215 | 216 | 217 | *Example: remote: "my_dev_server"* 218 | 219 | --- 220 | 221 | #### `parseJson` *boolean* 222 | When incoming request sent with type `application/json` Derver will parse its body and put object in `request.body`. 223 | 224 | 225 | *Default: true* 226 | 227 | 228 | *Example: parseJson: true* 229 | 230 | --- 231 | 232 | #### `preserveScroll` *boolean*|*number* 233 | 234 | Restore scroll position on every reload. Number value is equal timeout before scroll restoration. 235 | 236 | *Default: false* 237 | 238 | 239 | *Example: preserveScroll: true* 240 | 241 | 242 | *Example: preserveScroll: 100* 243 | 244 | --- 245 | 246 | #### `banner` *boolean* 247 | Show or not the banner in console when server starts. 248 | 249 | 250 | *Default: true* 251 | 252 | 253 | *Example: banner: false* 254 | 255 | --- 256 | 257 | #### `log` *boolean* 258 | Show or not file requests in console 259 | 260 | 261 | *Default: true* 262 | 263 | 264 | *Example: log: false* 265 | 266 | --- 267 | 268 | #### `onwatch` *function* 269 | This function will be called when any file changes in watched directories. 270 | 271 | 272 | *Default: null* 273 | 274 | 275 | *Example: onwatch: (liverload,watchitem)=>{if(watchitem == 'src') livereload.prevent()})* 276 | 277 | --- 278 | 279 | ### `onwatch`-callback arguments 280 | 281 | *Callback signature: (livereload,watchitem,filename,eventname)* 282 | 283 | #### `livereload` 284 | It is object with following methods: 285 | 286 | * `livereload.prevent()` - Will stop sheduled livereload action for this watch event. 287 | * `livereload.reload()` - Run each time you want to reload page in the browser. 288 | * `livereload.console(message)` - Send a `message` to the browser console. 289 | * `livereload.error(message,[header])` - Show error modal on client. 290 | 291 | --- 292 | 293 | #### `watchitem` 294 | It is a string with directory name where were fired filechange event. It is same string as you specified in `watch` option(or in `dir` option, if `watch` not set). 295 | 296 | --- 297 | 298 | #### `filename` 299 | Full path of changed file (unstable) 300 | 301 | --- 302 | 303 | #### `eventname` 304 | What exactly happened with modified file. 305 | 306 | 307 | ## Using Middlewares 308 | 309 | You may use any common middleware(like Express middlewares) to add additional functionality for you server. `derver()` function returns the object with methods: 310 | - `sub` - run callback to register middlewares for specified subpath 311 | - `use` - run middleware for all HTTP methods 312 | - `get` - run middleware for GET method only 313 | - `post` - run middleware for POST method only 314 | - ...actually you can write any HTTP method here 315 | 316 | ```js 317 | derver() 318 | .use(middleware1) 319 | .get('/api',middleware2) 320 | .put('/clear',middleware3,middleware4) 321 | 322 | ``` 323 | 324 | ### Pattern 325 | 326 | If first argument for these methods is a pattern of the URL, middleware will run only if request's URL is matched with its path. 327 | 328 | The pattern may looks like `/foo` of `/foo/bar`. If no pattern provided, middleware will run on each request. 329 | 330 | Pattern also may have a parameters `/user/:name` and when URL will be `/user/bob` or `/user/alex` you can get the value(alex or bob) from `request.params.name` property. 331 | 332 | ```js 333 | derver() 334 | .use('/user/:name',middleware) 335 | 336 | ``` 337 | 338 | ### Writing middleware function 339 | 340 | The middleware functions gets three arguments - common Node's `request`,`response` objects and `next` function, which will run next middleware. If your middleware doesn't response on request, you must run `next()` or request never will be ended. 341 | 342 | Lets see an example for client and API middleware: 343 | 344 | ```js 345 | function myLogMiddleware(req,resp,next){ 346 | console.log('Current URL is: ' + req.url); 347 | next(); 348 | } 349 | 350 | function myHelloMiddleware(req,resp){ 351 | resp.send('Hello, '+req.params.name); 352 | } 353 | 354 | derver() 355 | .use(myLogMiddleware) 356 | .get('/hello/:name',myHelloMiddleware) 357 | ``` 358 | 359 | ### Additional data from incoming request extension 360 | 361 | The `request` argument is Node's [http.IncomingMessage object](https://nodejs.org/api/http.html#http_class_http_incomingmessage). But it is expanded with few useful data: 362 | * `path` - pathname of current URL 363 | * `search` - query params as a string 364 | * `query` - query params as an object 365 | * `host` - host from header(including port) 366 | * `hostname` - host from header(without port) 367 | * `port` - port from header 368 | 369 | If request sent with type `application/json` you will get the parsed object from `request.body`. To avoid this, set `parseJson` option to `false`; 370 | 371 | ### Send JSON object in responce 372 | 373 | Method `responce.send(message)` will send content of message with status code 200. If `message` is a simple object, Derver will automaticly stringify it and send to client with `Content-type: application/json` header. 374 | 375 | ### Nested middlewares 376 | 377 | In case you need to run middlewares which are situated under specified sub path use `.sub()` method. 378 | 379 | ```js 380 | 381 | derver() 382 | .sub('/api',(app)=> 383 | // will run on every request starting with '/api/...' 384 | app.use(myLogMiddleware); 385 | 386 | // will run when URL will be '/api/hello/bob' 387 | app.get('/hello/bob',myHelloMiddleware); 388 | 389 | app.sub('/users',(app)=>{ 390 | // will run when URL will be '/api/users/add' 391 | app.post('/add',myUserAddMiddleware); 392 | }) 393 | }) 394 | 395 | ``` 396 | 397 | ## Remote control 398 | 399 | There is a way to perform some actions in currently opened browser windows. For example you want to reload page from external script. 400 | 401 | ```js 402 | import {createRemote} from 'derver'; 403 | 404 | // Create remote object with parameters of running server 405 | const remote = createRemote({host:'localhost',port:7000}); // there are defaults, may be dropped 406 | 407 | // Also you can call `createRemote('my_dev_server)` with server ID specified in remote option. 408 | 409 | // Reload page in all opened browser windows 410 | remote.reload(); 411 | 412 | // Send some text in the browser console 413 | remote.console('Hello!'); 414 | 415 | // Show modal with error message 416 | remote.error('Error happened! Fix it as soon as possible!','Error header'); 417 | ``` 418 | 419 | *Note: don't forget to enable `remote` option in Derver's configuration* 420 | 421 | 422 | ## How livereload works 423 | 424 | When you changes file in the watching directory, server will send command to the client side to reload current page. It is musthave feature when you developing web-application and want to see changes immediately. 425 | 426 | Livereload made with [Server Sent Events API](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). It is perfect feature for one-way communication server with client based only on http protocol. It is why Derver is so tiny, no need to implement websocket communication as others known servers do. 427 | 428 | Some JavaScript code for livereload will be added before `` element inside each requested `html` file. -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexxNB/derver/0fa09b7242074641d277862120437f02fedc2b37/demo.gif -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild'); 2 | const pkg = require('./package.json'); 3 | 4 | const DEV = process.argv.includes('--dev'); 5 | 6 | (async ()=>{ 7 | try{ 8 | // ES-module 9 | await build({ 10 | entryPoints: ['./src/index.js'], 11 | platform: 'node', 12 | format: "esm", 13 | outfile: pkg.module, 14 | minify: !DEV, 15 | bundle: true, 16 | }); 17 | 18 | //Node-module 19 | await build({ 20 | entryPoints: ['./src/index.js'], 21 | platform: 'node', 22 | format: "cjs", 23 | outfile: pkg.main, 24 | minify: !DEV, 25 | bundle: true, 26 | }); 27 | 28 | //Bin 29 | await build({ 30 | entryPoints: ['./src/bin.js'], 31 | platform: 'node', 32 | format: "cjs", 33 | outfile: pkg.bin.derver, 34 | minify: !DEV, 35 | bundle: true, 36 | external: [pkg.main] 37 | }); 38 | 39 | //Rollup plugin 40 | await build({ 41 | entryPoints: ['./src/plugins/rollup.js'], 42 | platform: 'node', 43 | format: "cjs", 44 | outfile: pkg.exports['./rollup-plugin'], 45 | minify: !DEV, 46 | bundle: true, 47 | external: ['.'] 48 | }); 49 | }catch(err){ 50 | console.log(err); 51 | process.exit(1); 52 | } 53 | })(); -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "derver", 3 | "version": "0.5.3", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "derver", 9 | "version": "0.5.3", 10 | "license": "MIT", 11 | "bin": { 12 | "derver": "bin/derver" 13 | }, 14 | "devDependencies": { 15 | "esbuild": "^0.12.29", 16 | "node-watch": "^0.7.2" 17 | } 18 | }, 19 | "node_modules/esbuild": { 20 | "version": "0.12.29", 21 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz", 22 | "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==", 23 | "dev": true, 24 | "hasInstallScript": true, 25 | "bin": { 26 | "esbuild": "bin/esbuild" 27 | } 28 | }, 29 | "node_modules/node-watch": { 30 | "version": "0.7.2", 31 | "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.2.tgz", 32 | "integrity": "sha512-g53VjSARRv1JdST0LZRIg8RiuLr1TaBbVPsVvxh0/0Ymvi0xYUjDuoqQQAWtHJQUXhiShowPT/aXKNeHBcyQsw==", 33 | "dev": true, 34 | "engines": { 35 | "node": ">=6" 36 | } 37 | } 38 | }, 39 | "dependencies": { 40 | "esbuild": { 41 | "version": "0.12.29", 42 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz", 43 | "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==", 44 | "dev": true 45 | }, 46 | "node-watch": { 47 | "version": "0.7.2", 48 | "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.2.tgz", 49 | "integrity": "sha512-g53VjSARRv1JdST0LZRIg8RiuLr1TaBbVPsVvxh0/0Ymvi0xYUjDuoqQQAWtHJQUXhiShowPT/aXKNeHBcyQsw==", 50 | "dev": true 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "derver", 3 | "version": "0.5.3", 4 | "description": "Tiny Development Server for your web-applications with livereload and watchers", 5 | "main": "./dist/derver.cjs.js", 6 | "module": "./dist/derver.esm.js", 7 | "bin": { 8 | "derver": "./bin/derver" 9 | }, 10 | "exports": { 11 | ".": { 12 | "node": "./dist/derver.cjs.js", 13 | "require": "./dist/derver.cjs.js", 14 | "import": "./dist/derver.esm.js" 15 | }, 16 | "./rollup-plugin": "./dist/plugins/rollup.js" 17 | }, 18 | "scripts": { 19 | "build": "node esbuild", 20 | "prestart": "node esbuild --dev", 21 | "start": "node test/test", 22 | "bin": "./bin/derver" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/AlexxNB/derver.git" 27 | }, 28 | "author": "Alexey Schebelev", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/AlexxNB/derver/issues" 32 | }, 33 | "homepage": "https://github.com/AlexxNB/derver#readme", 34 | "keywords": [ 35 | "server", 36 | "devserver", 37 | "livereload", 38 | "serve", 39 | "http-server", 40 | "http" 41 | ], 42 | "devDependencies": { 43 | "esbuild": "^0.12.29", 44 | "node-watch": "^0.7.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {derver} from './../dist/derver.cjs.js'; 3 | import {retrieveParams,exit,getUsageText} from './lib/bin'; 4 | 5 | const input = retrieveParams(); 6 | 7 | if(!input || input.params.help) { 8 | console.log(getUsageText()); 9 | exit(); 10 | } 11 | 12 | let options = {}; 13 | 14 | if(input.dir) options.dir = input.dir; 15 | if(input.params.index) options.index = input.params.index; 16 | if(input.params.watch) options.index = input.params.watch; 17 | if(input.params['no-watch']) options.watch = false; 18 | if(input.params.spa) options.spa = true; 19 | if(input.params.compress) options.compress = true; 20 | if(input.params.cache) options.cache = input.params.cache === true ? true : Number(input.params.cache); 21 | if(input.params.scroll) options.preserveScroll = input.params.scroll === true ? 10 : Number(input.params.scroll); 22 | if(input.params.production) { 23 | options.compress = true; 24 | options.cache = true; 25 | options.watch = false; 26 | options.host = '0.0.0.0'; 27 | } 28 | if(input.params.host) options.host = input.params.host; 29 | if(input.params.port) options.port = Number(input.params.port); 30 | 31 | derver(options); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {startHTTPServer,createMiddlwaresList} from './lib/http'; 2 | import {startWatchers} from './lib/watch'; 3 | export {createRemote} from './lib/livereload'; 4 | 5 | let default_options = { 6 | port: 7000, 7 | host: 'localhost', 8 | index: 'index.html', 9 | dir: 'public', 10 | compress: false, 11 | cache: false, 12 | spa: false, 13 | watch: null, 14 | onwatch: null, 15 | remote: false, 16 | parseJson: true, 17 | preserveScroll: false, 18 | banner: true, 19 | log: true 20 | } 21 | 22 | export function derver(options){ 23 | const opt = Object.assign(default_options,options,{middlewares:createMiddlwaresList()}); 24 | 25 | (async()=>{ 26 | if(opt.dir && opt.watch === null) opt.watch = opt.dir; 27 | 28 | try{ 29 | await startHTTPServer(opt); 30 | }catch(err){ 31 | console.log(err.message) 32 | process.exit(1); 33 | } 34 | 35 | startWatchers(opt); 36 | })() 37 | 38 | return opt.middlewares; 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/bin.js: -------------------------------------------------------------------------------- 1 | import params from './params.json'; 2 | 3 | export function retrieveParams(){ 4 | const result = { 5 | dir:'.', 6 | params:{} 7 | } 8 | 9 | let cli = process.argv.slice(2); 10 | 11 | if(cli.length == 0) return result; 12 | if(!cli[cli.length-1].startsWith('-')) result.dir = cli.pop(); 13 | 14 | for(let part of cli){ 15 | const pair = part.split('='); 16 | 17 | const name = pair[0].replace(/^\-{1,2}/,''); 18 | const value = pair[1] || true; 19 | const exists = name in result.params; 20 | 21 | let param = params.params.find( e => e.name == name ); 22 | if(!param) return false; 23 | 24 | if(param.multiple){ 25 | if(!exists) result.params[name] = []; 26 | result.params[name].push(value); 27 | }else{ 28 | if(exists) return false; 29 | result.params[name] = value; 30 | } 31 | } 32 | 33 | return result; 34 | } 35 | 36 | export function exit(code=0){ 37 | process.exit(code); 38 | } 39 | 40 | export function getUsageText(){ 41 | 42 | return `${params.description} 43 | 44 | Usage: 45 | ${params.name} ${params.usage} 46 | 47 | Parameters: 48 | ${params.params.map(p => ` ${p.name.padEnd(15)} ${p.help}`).join('\n')} 49 | 50 | Examples: 51 | ${params.examples.map(ex => ` ${params.name} ${ex}`).join('\n')} 52 | ` 53 | } -------------------------------------------------------------------------------- /src/lib/colors.js: -------------------------------------------------------------------------------- 1 | const color = (str,begin,end) => `\u001b[${begin}m${str}\u001b[${end}m` 2 | 3 | export default { 4 | blue: str => color(str,34,39), 5 | red: str => color(str,31,39), 6 | green: str => color(str,32,39), 7 | yellow: str => color(str,33,39), 8 | magenta: str => color(str,35,39), 9 | cyan: str => color(str,36,39), 10 | gray: str => color(str,90,39), 11 | 12 | bold: str => color(str,1,22), 13 | italic: str => color(str,3,23), 14 | } -------------------------------------------------------------------------------- /src/lib/http.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import fs from 'fs/promises'; 3 | import os from 'os'; 4 | import path from 'path'; 5 | import zlib from 'zlib'; 6 | import mime from './mime.json'; 7 | import c from './colors'; 8 | import {table} from './table'; 9 | import {mwLivereload, mwInjectLivereload} from './livereload'; 10 | import {version} from './../../package.json'; 11 | 12 | export function startHTTPServer(options){ 13 | const production = (options.watch === false && options.cache && options.compress); 14 | return new Promise(async (resolve,reject)=>{ 15 | 16 | const clearSID = await saveSID(options); 17 | 18 | const server = http.createServer(function (req, res) { 19 | 20 | let middlewares = [ 21 | mwURLParse(options), 22 | mwJsonParse(options), 23 | mwSend(options), 24 | mwServer(options) 25 | ]; 26 | 27 | middlewares = middlewares.concat(options.middlewares.list()); 28 | 29 | if(options.dir){ 30 | middlewares = middlewares.concat([ 31 | mwLivereload(options), 32 | mwFile(options), 33 | mwStatic(options), 34 | mwInjectLivereload(options) 35 | ]) 36 | } 37 | 38 | middlewares = middlewares.concat([ 39 | mwEncode(options), 40 | mwCache(options) 41 | ]) 42 | 43 | runMiddlewares(middlewares,req,res); 44 | }); 45 | 46 | server.on('listening',_ => { 47 | resolve(server); 48 | if(options.banner){ 49 | table() 50 | .line(production ? 'Derver server started' : 'Development server started','bold') 51 | .line('on') 52 | .line(`http://${options.host}:${options.port}`,'blue') 53 | .print(5,'green') 54 | } 55 | }) 56 | 57 | server.on('error',e => { 58 | console.log(c.bold('\n\nServer starting error:')); 59 | console.log(' '+c.red(e.toString()) + '\n\n'); 60 | reject(e.toString()); 61 | }) 62 | 63 | server.listen(options.port,options.host); 64 | 65 | const onclose = async ()=>{ 66 | await clearSID(); 67 | server.close(); 68 | } 69 | 70 | process.on('SIGTERM', onclose); 71 | process.on('exit', onclose); 72 | }); 73 | } 74 | 75 | export function createMiddlwaresList(){ 76 | const middlewares = []; 77 | 78 | function addMiddleware(obj){ 79 | 80 | for(let mw of obj.middlewares){ 81 | middlewares.push(function(req,res,next){ 82 | 83 | if(obj.method && obj.method !== req.method) return next(); 84 | 85 | if(obj.pattern && obj.pattern !== ''){ 86 | const match = getRouteMatch(obj.pattern,req.path); 87 | if(!match || (obj.exact && !match.exact)) return next(); 88 | req.params = match.params; 89 | } 90 | mw(req,res,next); 91 | }); 92 | } 93 | } 94 | 95 | function parseArguments(args,name,pattern){ 96 | args = Array.from(args); 97 | let subpattern = args.length > 0 && typeof args[0] == 'string' ? args.shift() : null; 98 | if(subpattern && !subpattern.startsWith('/')) subpattern = '/'+subpattern; 99 | return { 100 | method: name == 'use' ? null : name.toUpperCase(), 101 | pattern: (pattern||'')+(subpattern||''), 102 | exact:!(pattern && !subpattern), 103 | middlewares: args.filter(fn => typeof fn == 'function') 104 | } 105 | } 106 | 107 | function getMethods(pattern){ 108 | const methods = new Proxy({},{ 109 | get(_, name) { 110 | if(name == 'list') return ()=>middlewares; 111 | if(name == 'sub') return function(){ 112 | let args = Array.from(arguments); 113 | let parentPattern = (pattern||'')+args.shift(); 114 | args.forEach(fn => fn(getMethods(parentPattern))); 115 | }; 116 | return function(){ 117 | addMiddleware(parseArguments(arguments,name,pattern)); 118 | return methods; 119 | }; 120 | } 121 | }); 122 | return methods; 123 | } 124 | 125 | 126 | return getMethods(); 127 | } 128 | 129 | function runMiddlewares(mwArray,req,res){ 130 | 131 | mwArray.push((req,res)=>res.end(res.body||'')); 132 | 133 | const next = ()=>{ 134 | let mw; 135 | while(!mw && mwArray.length > 0){ 136 | mw = mwArray.shift(); 137 | } 138 | mw && mw(req,res,next); 139 | } 140 | 141 | next(); 142 | } 143 | 144 | function mwURLParse(options){ 145 | return function(req,res, next){ 146 | const parts = new URL(req.url,'http://'+(req.headers.host || 'derver.tld')); 147 | req.path = parts.pathname; 148 | req.host = parts.host; 149 | req.hostname = parts.hostname; 150 | req.port = parts.port; 151 | req.search = parts.search; 152 | req.query = Array.from(parts.searchParams).reduce((obj,[name,value])=>(obj[name]=value,obj),{}); 153 | next(); 154 | } 155 | } 156 | 157 | function mwJsonParse(options){ 158 | return function(req,res, next){ 159 | if(options.parseJson){ 160 | const isJson = req.headers['content-type'] && !req.headers['content-type'].indexOf('application/json') ; 161 | 162 | if(isJson){ 163 | let data = ''; 164 | req.on('data', chunk => { 165 | data += chunk; 166 | }) 167 | req.on('end', () => { 168 | try{ 169 | req.body = JSON.parse(data); 170 | }catch(err){ 171 | req.body = {}; 172 | console.log(err.message); 173 | } 174 | next(); 175 | }); 176 | } else next(); 177 | } else next(); 178 | } 179 | } 180 | 181 | function mwFile(options){ 182 | return async function(req, res, next){ 183 | req.file = path.join(options.dir,req.path); 184 | req.extname = path.extname(req.file); 185 | 186 | if(req.extname === ''){ 187 | req.file = path.join(req.file,options.index); 188 | req.extname = path.extname(req.file); 189 | } 190 | 191 | req.exists = await isExists(req.file); 192 | 193 | if(options.spa && !req.exists && req.extname === path.extname(options.index)){ 194 | console.log() 195 | let dir = path.dirname(req.file); 196 | do{ 197 | dir = path.dirname(dir) 198 | req.file = path.join(dir,options.index); 199 | if(req.exists = await isExists(req.file)) break; 200 | } while(dir !== '.') 201 | 202 | } 203 | 204 | next(); 205 | } 206 | } 207 | 208 | function mwSend(options){ 209 | return function(req,res, next){ 210 | res.send = function(message){ 211 | let mime = 'text/plain'; 212 | if(typeof message == 'object'){ 213 | message = JSON.stringify(message); 214 | mime = 'application/json' 215 | } 216 | res.writeHead(200,{'Content-Type':mime}); 217 | res.end(message); 218 | } 219 | next(); 220 | } 221 | } 222 | 223 | 224 | 225 | function mwServer(options){ 226 | return function(req,res, next){ 227 | res.setHeader('Server', 'Derver/'+version); 228 | next(); 229 | } 230 | } 231 | 232 | function mwStatic(options){ 233 | 234 | return async function(req,res,next){ 235 | 236 | if(!req.exists){ 237 | options.log && console.log(c.gray(' [web] ')+req.url + ' - ' + c.red('404 Not Found')); 238 | res.writeHead(404, {'Content-Type': 'text/plain'}); 239 | return res.end('Not found'); 240 | } 241 | 242 | if(mime[req.extname]) res.setHeader('Content-Type', mime[req.extname]); 243 | 244 | res.body = await fs.readFile(req.file); 245 | options.log && console.log(c.gray(' [web] ')+req.url + ' - ' + c.green('200 OK')); 246 | next(); 247 | } 248 | } 249 | 250 | 251 | function mwEncode(options){ 252 | 253 | if(!options.compress) return null; 254 | 255 | return function(req,res,next){ 256 | if(req.headers['accept-encoding']){ 257 | if(req.headers['accept-encoding'].includes('br')){ 258 | res.setHeader('Content-Encoding', 'br'); 259 | res.body = zlib.brotliCompressSync(res.body); 260 | }else if(req.headers['accept-encoding'].includes('gzip')){ 261 | res.setHeader('Content-Encoding', 'gzip'); 262 | res.body = zlib.gzipSync(res.body); 263 | } 264 | } 265 | next(); 266 | } 267 | } 268 | 269 | function mwCache(options){ 270 | 271 | if(!options.cache) return null; 272 | 273 | return function(req,res,next){ 274 | if(typeof options.cache !== 'number') options.cache = 31536000; 275 | res.setHeader('Cache-Control', 'max-age='+options.cache); 276 | next(); 277 | } 278 | } 279 | 280 | export function getRouteMatch(pattern,path){ 281 | pattern = pattern.endsWith('/') ? pattern : pattern + '/'; 282 | path = path.endsWith('/') ? path : path + '/'; 283 | const keys = []; 284 | let params = {}; 285 | let exact = true; 286 | let rx = pattern 287 | .split('/') 288 | .map(s => s.startsWith(':') ? (keys.push(s.slice(1)),'([^\\/]+)') : s) 289 | .join('\\/'); 290 | 291 | let match = path.match(new RegExp(`^${rx}$`)); 292 | if(!match) { 293 | exact = false; 294 | match = path.match(new RegExp(`^${rx}`)); 295 | } 296 | if(!match) return null; 297 | keys.forEach((key,i) => params[key] = match[i+1]); 298 | 299 | return { 300 | exact, 301 | params, 302 | part:match[0].slice(0,-1) 303 | } 304 | } 305 | 306 | async function saveSID(options){ 307 | const tmp = os.tmpdir(); 308 | if(typeof options.remote !== 'string') return ()=>{}; 309 | const file = path.join(tmp,'derver_'+options.remote); 310 | await fs.writeFile(file,JSON.stringify({host:options.host,port:options.port})); 311 | return async ()=>await fs.unlink(file); 312 | } 313 | 314 | async function isExists(file){ 315 | try{ 316 | await fs.stat(file); 317 | return true 318 | }catch{ 319 | return false 320 | } 321 | } -------------------------------------------------------------------------------- /src/lib/livereload.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import os from 'os'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | 6 | const LR_URL = '/derver-livereload-events'; 7 | const LR_REMOTE_URL = '/derver-livereload-remote'; 8 | 9 | const listeners = new Set(); 10 | 11 | export function livereload(event,p1,p2){ 12 | listeners.forEach(listener=>{ 13 | if(typeof listener[event] == 'function') listener[event](p1,p2); 14 | }); 15 | } 16 | 17 | export function createRemote(options){ 18 | 19 | const remoteID = typeof options == 'string' ? options : false; 20 | 21 | let host = 'localhost'; 22 | let port = 7000; 23 | 24 | if(!remoteID){ 25 | options && options.host && (host = options.host) 26 | options && options.port && (port = options.port) 27 | } 28 | 29 | function sendCommand(command,data){ 30 | return new Promise((resolve)=>{ 31 | 32 | const config = remoteID && getRemoteConfig(remoteID); 33 | config && config.host && (hostname = config.host); 34 | config && config.port && (port = config.port) 35 | 36 | const req_options = { 37 | hostname: (config && config.host) || host, 38 | port: (config && config.port) || port, 39 | path: LR_REMOTE_URL, 40 | method: 'POST', 41 | headers: { 42 | 'Content-Type': 'application/json', 43 | } 44 | } 45 | const req = http.request(req_options, (res)=>{ 46 | res.on('data',(chunk)=>{ 47 | if(chunk.toString()==='REMOTE OK') 48 | resolve('OK') 49 | else { 50 | console.log('[Derver remote]: Warning: wrong command ' + command) 51 | resolve('WARNING'); 52 | } 53 | }); 54 | }); 55 | req.on('error', (e)=>{ 56 | console.log('[Derver remote]: Warning:' + e.message) 57 | resolve('WARNING'); 58 | }); 59 | req.write(JSON.stringify({command,data:data||{}})); 60 | req.end(); 61 | }); 62 | } 63 | 64 | return { 65 | reload(){return sendCommand('reload')}, 66 | console(text){return sendCommand('console',{text})}, 67 | error(text,header){return sendCommand('error',{text,header})} 68 | } 69 | } 70 | 71 | export function mwLivereload(options){ 72 | if(!options.watch && !options.remote) return null; 73 | return function(req,res,next){ 74 | if(req.url == LR_URL){ 75 | 76 | const write = (evnt,data)=>res.write(`event: ${evnt}\ndata: ${JSON.stringify(data||{})}\n\n`) 77 | 78 | const listener = { 79 | reload: ()=>write('refresh'), 80 | console: (text)=>write('console',{text}), 81 | error: (text,header)=>write('srverror',{text,header:(header||'Error')}), 82 | } 83 | 84 | listeners.add(listener); 85 | 86 | res.writeHead(200, { 87 | 'Content-Type': 'text/event-stream', 88 | 'Cache-Control': 'no-cache', 89 | 'Connection': 'keep-alive', 90 | }); 91 | 92 | res.on('close', function() { 93 | listeners.delete(listener); 94 | }); 95 | res.write('data: connected\n\n') 96 | }else if(options.remote && req.url == LR_REMOTE_URL){ 97 | if(req.method == 'POST'){ 98 | let json = ''; 99 | 100 | req.on('data', chunk => { 101 | json += chunk.toString(); 102 | }); 103 | 104 | req.on('end', () => { 105 | const request = JSON.parse(json||'{}'); 106 | 107 | if(request.command == 'reload') livereload('reload'); 108 | else if(request.command == 'console') livereload('console',request.data.text); 109 | else if(request.command == 'error') livereload('error',request.data.text,request.data.header); 110 | else return res.end('REMOTE WRONG COMMAND'); 111 | 112 | res.end('REMOTE OK'); 113 | }); 114 | }else next(); 115 | } else next(); 116 | } 117 | } 118 | 119 | export function getLrURL(){ 120 | return LR_URL; 121 | } 122 | 123 | export function getRemoteURL(){ 124 | return LR_REMOTE_URL; 125 | } 126 | 127 | // Must be a clean function. Will be injected in browser client in \n$1` 270 | ) 271 | ); 272 | } 273 | 274 | next(); 275 | } 276 | } 277 | 278 | function getRemoteConfig(name){ 279 | const tmp = os.tmpdir(); 280 | const file = path.join(tmp,'derver_'+name); 281 | if(!fs.existsSync(file)) return false; 282 | return JSON.parse(fs.readFileSync(file,'utf-8')); 283 | } -------------------------------------------------------------------------------- /src/lib/mime.json: -------------------------------------------------------------------------------- 1 | { 2 | ".htm": "text/html", 3 | ".html": "text/html", 4 | ".jpg": "image/jpeg", 5 | ".jpeg": "image/jpeg", 6 | ".gif": "image/gif", 7 | ".png": "image/png", 8 | ".svg": "image/svg+xml", 9 | ".js": "text/javascript", 10 | ".json": "application/json", 11 | ".css": "text/css", 12 | ".ico": "image/x-icon" 13 | } -------------------------------------------------------------------------------- /src/lib/params.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "derver", 3 | "description": "Derver is a tiny development web server with livereload and files watching.", 4 | "usage":"[params] [serve dir]", 5 | "params":[ 6 | { 7 | "name": "host", 8 | "help": "Host where to bind server" 9 | }, 10 | { 11 | "name": "port", 12 | "help": "Port where to bind server" 13 | }, 14 | { 15 | "name": "index", 16 | "help": "Index file for server" 17 | }, 18 | { 19 | "name": "watch", 20 | "multiple": true, 21 | "help": "Directory for watching. May be used multiple times." 22 | }, 23 | { 24 | "name": "no-watch", 25 | "help": "Prevent any watching." 26 | }, 27 | { 28 | "name": "compress", 29 | "help": "Return files compressed by gzip or brotli, if client supports it." 30 | }, 31 | { 32 | "name": "cache", 33 | "help": "Add Cache-control header." 34 | }, 35 | { 36 | "name": "spa", 37 | "help": "Enaples SPA mode when all page requests will be responced by index page." 38 | }, 39 | { 40 | "name": "production", 41 | "help": "Run server in production mode: enables --compress,--cache and--no-watch parameters" 42 | }, 43 | { 44 | "name": "help", 45 | "help": "Show usage information" 46 | } 47 | ], 48 | "examples":[ 49 | "", 50 | "dist/public", 51 | "--host=0.0.0.0 --port=5000", 52 | "--watch=dist/public --watch=src", 53 | "--production" 54 | ] 55 | } -------------------------------------------------------------------------------- /src/lib/table.js: -------------------------------------------------------------------------------- 1 | import c from './colors'; 2 | 3 | export function table(){ 4 | let width = 2; 5 | let lines = []; 6 | 7 | const t = { 8 | line: (text='',color1,color2)=>{ 9 | const len = text.length; 10 | if(len+2 > width) width = len+2; 11 | 12 | if(color1) text = c[color1](text); 13 | if(color2) text = c[color2](text); 14 | 15 | lines.push([text,len]); 16 | return t; 17 | }, 18 | print: (ident=0,color)=>{ 19 | let margin = ' '.repeat(ident); 20 | 21 | let top = `${margin}╔${'═'.repeat(width)}╗`; 22 | let bottom = `${margin}╚${'═'.repeat(width)}╝`; 23 | let left = `${margin}║`; 24 | let right = `║`; 25 | 26 | if(color){ 27 | top = c[color](top); 28 | bottom = c[color](bottom); 29 | left = c[color](left); 30 | right = c[color](right); 31 | } 32 | 33 | console.log(top); 34 | for(let line of lines){ 35 | const l = Math.floor((width-line[1])/2); 36 | const r = width-line[1]-l; 37 | console.log(`${left}${' '.repeat(l)}${line[0]}${' '.repeat(r)}${right}`); 38 | } 39 | console.log(bottom); 40 | return t; 41 | } 42 | } 43 | 44 | return t; 45 | } -------------------------------------------------------------------------------- /src/lib/watch.js: -------------------------------------------------------------------------------- 1 | import c from './colors'; 2 | import {livereload} from './livereload'; 3 | import watch from 'node-watch'; 4 | 5 | export function startWatchers(options){ 6 | 7 | if(typeof options.watch === 'string') options.watch = [options.watch]; 8 | 9 | if(options.watch){ 10 | 11 | console.log(c.yellow(' Waiting for changes...\n\n')); 12 | 13 | const watchers = []; 14 | 15 | process.on('SIGTERM', ()=>watchers.forEach(w=>w.close())); 16 | process.on('exit', ()=>watchers.forEach(w=>w.close())); 17 | 18 | const cooldown = new Set(); 19 | const debounce = (key, fn)=>{ 20 | if(cooldown.has(key)) return; 21 | cooldown.add(key); 22 | setTimeout(()=>cooldown.delete(key),100); 23 | fn(); 24 | } 25 | 26 | for(let watchitem of options.watch){ 27 | watchers.push( 28 | watch(watchitem,{recursive: true}, async function(evt, name) { 29 | 30 | debounce(watchitem,()=>console.log(c.gray('[watch] ')+'Changes in ' + c.blue(watchitem))); 31 | 32 | let lrFlag = true; 33 | if(typeof options.onwatch === 'function'){ 34 | 35 | await options.onwatch({ 36 | prevent: ()=>lrFlag=false, 37 | reload: ()=>livereload('reload'), 38 | console: (str)=>livereload('console',str), 39 | error: (str,header)=>livereload('error',str,header), 40 | },watchitem,name,evt) 41 | } 42 | if(lrFlag) livereload('reload'); 43 | }) 44 | ); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/plugins/rollup.js: -------------------------------------------------------------------------------- 1 | import {derver as server} from './../..'; 2 | 3 | export function derver(options){ 4 | let first = true; 5 | return { 6 | name: 'rollup-plugin-derver', 7 | generateBundle () { 8 | if (!first) return; 9 | first = !first; 10 | server(options); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /test/public/assets/style.css: -------------------------------------------------------------------------------- 1 | h1{ 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test-app 5 | 6 | 7 | 20 | 21 |

Test Derver

22 | 29 | 30 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /test/public/subdir/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test-app subdir 5 | 6 | 7 | 8 |

Test Derver

9 |
10 |

This is subdir index.html

11 |
12 | 13 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const {derver,createRemote} = require('./../dist/derver.cjs.js'); 2 | 3 | const app = derver({ 4 | dir: 'test/public', 5 | spa: true, 6 | remote: 'testname', 7 | banner: false, 8 | log: false, 9 | preserveScroll: 10, 10 | // watch: false, 11 | // cache: true, 12 | // compress: true, 13 | onwatch: (livereload,watcher,file,evt)=>{ 14 | console.log('Hello',watcher,file,evt); 15 | // livereload.prevent(); livereload.console('Hello'); 16 | // livereload.prevent(); livereload.error('Error text','Build error'); 17 | 18 | } 19 | }) 20 | /* 21 | app.use((req,res,next)=>{ 22 | console.log('HELLO'); 23 | next(); 24 | }); 25 | 26 | 27 | app.get('/hello/:name',(req,res,next)=>{ 28 | console.log('HELLO2'); 29 | res.writeHead(200); 30 | res.end('Hello,'+req.params.name+'!'); 31 | }); 32 | 33 | app.post('/jsontest',(req,res,next)=>{ 34 | console.log(req.body); 35 | res.send({message: 'Hello '+req.body.name}); 36 | }); 37 | 38 | app.sub('/test',a => { 39 | a.get('/',(req,res,next)=>{ 40 | console.log('HELLO4'); 41 | next(); 42 | }) 43 | a.get('/:name',(req,res)=>{ 44 | console.log('HELLO3'); 45 | res.writeHead(200); 46 | res.end('Hello,'+req.params.name+'!'); 47 | }) 48 | }) 49 | /* 50 | const remote = createRemote('testname'); 51 | setTimeout(()=>remote.error('Hello from remote','Header here'),4000); 52 | */ --------------------------------------------------------------------------------