├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── bin ├── help.txt └── index.js ├── doc ├── floating.svg ├── github.svg ├── global.css ├── index.html ├── logo.png ├── logo.svg ├── noize.png ├── priority.svg ├── shadow.svg └── system.svg ├── index.js ├── lib ├── config-builder.js ├── handler.js ├── mkdirs.js ├── refresh.js ├── server.js ├── static-export.js └── watch.js ├── package.json └── test ├── deeper └── ref.js ├── expect ├── a.js ├── images │ └── logo.png └── index.html ├── fixture ├── a.js ├── b.js ├── images │ └── logo.png └── index.html └── spec.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "indent": [ 12 | "error", 13 | 2 14 | ], 15 | "linebreak-style": [ 16 | "error", 17 | "unix" 18 | ], 19 | "no-console": [ 20 | "error", 21 | { "allow": ["warn", "log"] } 22 | ], 23 | "quotes": [ 24 | "error", 25 | "single", 26 | "avoid-escape" 27 | ], 28 | "semi": [ 29 | "error", 30 | "never" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | cache 4 | dist 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.*" 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Tsutomu Kawamura 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Felt](doc/logo.png) 2 | 3 | [![Build Status][travis-image]][travis-url] 4 | 5 | On-demand bundler for ES6 / CSS Next. 6 | 7 | - [Use Felt as a webserver via CLI](#cli-usages) 8 | - [Use Felt as a middleware of Express](#with-express) 9 | 10 | See also its [options](#options), [plugins](#plugins) and [recipes](#recipes). 11 | 12 | *Note: Felt runs on Node 6.x or above.* 13 | 14 | ## CLI usages 15 | 16 | In short, install Felt globally: 17 | 18 | ```bash 19 | $ npm install -g felt felt-recipe-minimal 20 | ``` 21 | 22 | *Note: `felt-recipe-minimal` is a recipe for Felt. You may install other recipes, too.* 23 | 24 | And run Felt: 25 | 26 | ```bash 27 | $ cd path/to/project/ 28 | $ felt 29 | ``` 30 | 31 | Then, open your site in browser: `http://localhost:3000`. 32 | 33 | *Note: type `Ctrl + C` to stop the server* 34 | 35 | ### Run Felt 36 | 37 | Assume that you have a project like this: 38 | 39 | - project/ 40 | - public/ 41 | - index.html 42 | - main.js 43 | - style.css 44 | - cache/ 45 | - package.json 46 | 47 | Then run Felt: 48 | 49 | ```bash 50 | $ cd path/to/project/ 51 | $ felt --recipe minimal --src public 52 | ``` 53 | 54 | There're some [official recipes](#recipes). Check them, too. 55 | 56 | ### Use config files 57 | 58 | Assume that you have a project like this: 59 | 60 | - project/ 61 | - public/ 62 | - index.html 63 | - main.js 64 | - style.css 65 | - cache/ 66 | - package.json 67 | - felt.config.js 68 | 69 | Or choose your own config file: 70 | 71 | ```bash 72 | $ felt --config felt.config.js 73 | ``` 74 | 75 | The default name of `config` is `felt.config.js`, so it's also fine: 76 | 77 | ```bash 78 | $ felt --config 79 | ``` 80 | 81 | The config file could be like this: 82 | 83 | ```javascript 84 | 'use strict' 85 | const 86 | rollup = require('felt-rollup'), 87 | buble = require('rollup-plugin-buble'), 88 | resolve = require('rollup-plugin-node-resolve'), 89 | commonjs = require('rollup-plugin-commonjs') 90 | 91 | module.exports = { 92 | src: 'public', 93 | handlers: { 94 | '.js': rollup({ 95 | plugins: [ 96 | resolve({ jsnext: true, main: true, browser: true }), 97 | commonjs(), 98 | buble() 99 | ], 100 | sourceMap: true 101 | }) 102 | } 103 | } 104 | ``` 105 | 106 | See more detail about [options](#options) 107 | 108 | ### Change port 109 | 110 | The default port is `3000`. If you want to change it, use `--port` option: 111 | 112 | ```bash 113 | $ felt --port 3333 114 | ``` 115 | 116 | *Note: you can set the port option in your config file, too.* 117 | 118 | ### Watch changes 119 | 120 | ```bash 121 | $ felt --src public --watch 122 | ``` 123 | 124 | ### Export static files 125 | 126 | This is handy to upload the contents to amazon S3 or GitHub Pages. Felt exports not only compiled files but also other assets like HTML, PNG, ...etc, too. 127 | 128 | ```bash 129 | $ felt --src public --export dist 130 | ``` 131 | 132 | *Note: with export option, Felt is not run as a server. It works as just a bundler.* 133 | 134 | ## With Express 135 | 136 | Install Felt and use it as an `express` middleware. 137 | 138 | ```bash 139 | $ npm install --save felt 140 | ``` 141 | 142 | Add `server.js` to the project: 143 | 144 | ```javascript 145 | const 146 | express = require('express'), 147 | felt = require('felt'), 148 | recipe = require('felt-recipe-minimal') 149 | 150 | const app = express() 151 | 152 | app.use(felt(recipe, { src: 'public' })) 153 | app.use(express.static('public')) 154 | app.listen(3000) 155 | ``` 156 | 157 | ### Separated config files 158 | 159 | It's a good idea to separate the config from `server.js`: 160 | 161 | ```javascript 162 | const 163 | express = require('express'), 164 | felt = require('felt') 165 | config = require('./felt.config.js') 166 | 167 | app.use(felt(config)) 168 | app.use(express.static('public')) 169 | app.listen(3000) 170 | ``` 171 | 172 | `felt.config.js` could be like this: 173 | 174 | ```javascript 175 | const 176 | rollup = require('felt-rollup'), 177 | buble = require('rollup-plugin-buble'), 178 | resolve = require('rollup-plugin-node-resolve'), 179 | commonjs = require('rollup-plugin-commonjs') 180 | 181 | module.exports = {/* options */} 182 | ``` 183 | 184 | 185 | ## Options 186 | 187 | property | default | descriptions 188 | :-- | :-- | :-- 189 | **opts.src** | `.` | the document directory to serve 190 | **opts.cache** | `'cache'` | if it's located inside `src`, ignored on requests 191 | **opts.root** | `process.cwd()` | usually no need to set it 192 | **opts.handlers** | `{}` | see the section below 193 | **opts.patterns** | `[]` | see the section below 194 | **opts.excludes** | `[]` | see the section below 195 | **opts.external** | `{}` | see the section below 196 | **opts.update** | `'once'` | `'never'` or `'always'` 197 | **opts.refresh** | `true` | set `false` to skip refreshing on starting 198 | **opts.watch** | `false` | set `true` to detect changes 199 | **opts.debug** | `false` | set `true` to show debug comments on the terminal 200 | 201 | ### opts.handlers 202 | 203 | Default handlers for each extension. 204 | 205 | ```javascript 206 | { 207 | handlers: { 208 | '*.js': rollup({ 209 | plugins: [ 210 | resolve({ jsnext: true }), 211 | commonjs(), 212 | buble() 213 | ], 214 | sourceMap: true 215 | }) 216 | } 217 | } 218 | ``` 219 | 220 | ### opts.patterns 221 | 222 | This option limits the target which Felt works with. This is handy when you want to use Felt for only a few files like this: 223 | 224 | ```javascript 225 | { 226 | patterns: ['index.js', 'components/*.js'] 227 | } 228 | ``` 229 | 230 | Which handler will be used is depends on the extension. If no handler for the extension, Felt will throw exceptions. 231 | 232 | You can also specify the custom handler for the pattern: 233 | 234 | ```javascript 235 | { 236 | patterns: [ 237 | 'index.js', 238 | { 239 | pattern: 'components/*.js', 240 | handler: rollup({ 241 | plugins: [babel()], 242 | sourceMap: true 243 | }) 244 | } 245 | ] 246 | } 247 | ``` 248 | 249 | ### opts.excludes 250 | 251 | This option excludes the files from compiling and copying (when exporting). Cache directory and `'node_modules/**'` are always excluded. For example: 252 | 253 | ```javascript 254 | { 255 | excludes: ['no-compile/**'] 256 | } 257 | ``` 258 | 259 | ### opts.external 260 | 261 | This option makes copies from deeper files typically in `node_modules`. For example, if you need to access `node_modules/pouchdb/dist/pouchdb.min.js`, you may write like this: 262 | 263 | ```javascript 264 | { 265 | external: { 266 | 'pouchdb.js': 'node_modules/pouchdb/dist/pouchdb.min.js' 267 | // 'where/to/expose': 'path/from/opts.root' 268 | } 269 | } 270 | ``` 271 | 272 | Then you can access it by `http://localhost:3000/pouchdb.js`. This option is convenient to directly expose a JavaScript or CSS file which comes from `npm` or `bower`. 273 | 274 | **Note**: The files will not be processed by `opts.handlers`. This means that the file will skip compiling by `rollup` and so on. 275 | 276 | ## Plugins 277 | 278 | Plugins are the interface to compilers like Rollup or PostCSS. Actually, these are the thin wrapper of these libraries: 279 | 280 | - [felt-rollup](https://github.com/cognitom/felt-rollup): JavaScript bundler 281 | - [felt-postcss](https://github.com/cognitom/felt-postcss): CSS bundler 282 | 283 | ## Recipes 284 | 285 | Recipes are pre-made configurations. You can use these recipes with some overwrite with ease. 286 | 287 | - [felt-recipe-minimal](https://github.com/cognitom/felt-recipe-minimal): PostCSS and Rollup with Bublé 288 | - [felt-recipe-standard](https://github.com/cognitom/felt-recipe-standard): PostCSS and Rollup with Babel 289 | - [felt-recipe-riot](https://github.com/cognitom/felt-recipe-riot): PostCSS and Rollup with Bublé + Riot 290 | - [felt-recipe-react](https://github.com/cognitom/felt-recipe-react): PostCSS and Rollup with Bublé + JSX 291 | - [felt-recipe-preact](https://github.com/ezekielchentnik/felt-recipe-preact): PostCSS and Rollup with Bublé + Preact *by [@ezekielchentnik](https://github.com/ezekielchentnik)* 292 | 293 | *Note: the repository name of the recipe supposed to have prefix `felt-recipe-`.* 294 | 295 | ## License 296 | 297 | MIT © Tsutomu Kawamura 298 | 299 | [travis-image]:https://img.shields.io/travis/cognitom/felt.svg?style=flat-square 300 | [travis-url]:https://travis-ci.org/cognitom/felt 301 | -------------------------------------------------------------------------------- /bin/help.txt: -------------------------------------------------------------------------------- 1 | Usage 2 | $ felt 3 | 4 | Options 5 | --recipe, -r Recipe for Felt (felt-recipe-*) 6 | --config, -c Path to config 7 | --src, -s Document directory 8 | --cache Cache directory 9 | --root Root directory of the project 10 | --update, -u never, once or always 11 | --refresh Starts with refreshing 12 | --no-refresh Starts without refreshing 13 | --watch, -w Watches files 14 | --debug Shows debugging comment 15 | 16 | Examples 17 | $ felt --recipe minimal --src public 18 | $ felt --config felt.config.js 19 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | const 4 | co = require('co'), 5 | meow = require('meow'), 6 | fsp = require('fs-promise'), 7 | path = require('path'), 8 | server = require('../lib/server'), 9 | staticExport = require('../lib/static-export'), 10 | configBuilder = require('../lib/config-builder') 11 | 12 | co(function* () { 13 | let recipe = {}, config = {}, flavor = { port: 3000 } 14 | const 15 | root = process.cwd(), 16 | helpFile = path.join(__dirname, 'help.txt'), 17 | helpText = yield fsp.readFile(helpFile, 'utf8'), 18 | cli = meow(helpText, { 19 | alias: { 20 | r: 'recipe', 21 | c: 'config', 22 | s: 'src', 23 | u: 'update', 24 | w: 'watch', 25 | p: 'port', 26 | e: 'export' 27 | }, 28 | string: ['recipe', 'config', 'src', 'cache', 'root', 'port', 'export', 'update'], 29 | boolean: ['refresh', 'no-refresh', 'watch', 'no-watch', 'debug'] 30 | }), 31 | flags = cli.flags 32 | 33 | if (!flags.recipe && !flags.config) flags.recipe = 'minimal' 34 | 35 | if (flags.recipe) { 36 | const pkgName = 'felt-recipe-' + flags.recipe 37 | try { 38 | recipe = require(pkgName) 39 | } catch(err) { 40 | throw new Error(`${ pkgName } is not installed`) 41 | } 42 | } 43 | 44 | if (flags.config !== undefined) { 45 | try { 46 | const configFile = path.resolve(root, flags.config || 'felt.config.js') 47 | config = require(configFile) 48 | } catch(err) { 49 | throw new Error('No valid config file') 50 | } 51 | } 52 | 53 | if (flags.src) flavor.src = flags.src 54 | if (flags.cache) flavor.cache = flags.cache 55 | if (flags.root) flavor.root = flags.root 56 | if (flags.update) flavor.update = flags.update 57 | if (flags.refresh) flavor.refresh = true 58 | if (flags.noRefresh) flavor.refresh = false 59 | if (flags.watch) flavor.watch = true 60 | if (flags.noWatch) flavor.watch = false 61 | if (flags.debug) flavor.debug = true 62 | if (flags.port) flavor.port = parseInt(flags.port) 63 | if (flags.export) flavor.cache = flags.export 64 | 65 | const opts = configBuilder(recipe, config, flavor) 66 | 67 | if (flags.export) staticExport(opts) 68 | else server(opts) 69 | }) 70 | -------------------------------------------------------------------------------- /doc/floating.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | floating 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /doc/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | github 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /doc/global.css: -------------------------------------------------------------------------------- 1 | /* reset */ 2 | @import 'normalize-css/normalize.css'; 3 | 4 | /* fonts */ 5 | @import 'https://fonts.googleapis.com/css?family=Arvo'; 6 | 7 | body { 8 | font-size: 21px; 9 | color: #71746C; 10 | } 11 | 12 | h1 { 13 | background-image: url(logo.svg); 14 | background-position: center; 15 | background-repeat: no-repeat; 16 | color: transparent; 17 | height: 73px; 18 | margin: 4em 0 0; 19 | padding: 0; 20 | } 21 | h2 { 22 | margin: 0 auto 2em; 23 | text-align: center; 24 | font-size: 135%; 25 | font-weight: normal; 26 | font-family: "Arvo", sans-serif; 27 | max-width: 20em; 28 | } 29 | h4 { 30 | margin: 0 auto; 31 | padding: 0; 32 | font-weight: normal; 33 | max-width: 15em; 34 | } 35 | code, 36 | pre { 37 | font-size: 85%; 38 | padding: .05em .2em; 39 | background: #EFF2EC; 40 | border-radius: 2px; 41 | } 42 | a { 43 | color: #6BC657; 44 | text-decoration: none; 45 | } 46 | 47 | body > header { 48 | background: #6BC657; 49 | background: #6BC657 url(noize.png); 50 | color: white; 51 | text-align: center; 52 | padding: 1px 0 0; 53 | position: relative; 54 | } 55 | body > header > p { 56 | font-family: "Avenir Next", sans-serif; 57 | font-size: 80%; 58 | } 59 | body > header > div.left, 60 | body > header > div.right { 61 | font-family: "Arvo"; 62 | font-size: 130%; 63 | padding: 2em; 64 | } 65 | body > header > div.left { 66 | text-align: left; 67 | padding-bottom: 5em; 68 | } 69 | body > header > div.right { 70 | text-align: right; 71 | padding-top: 1.5em; 72 | background: #BBC657; 73 | background: #BBC657 url(noize.png); 74 | color: #71746C; 75 | } 76 | body > header > div.floating, 77 | body > header > div.shadow { 78 | position: absolute; 79 | width: 334px; 80 | height: 244px; 81 | background-position: center; 82 | background-repeat: no-repeat; 83 | left: 50%; 84 | margin-left: -167px; 85 | } 86 | body > header > div.floating { 87 | background-image: url(floating.svg); 88 | bottom: 8em; 89 | animation-duration: 5s; 90 | animation-name: bounce; 91 | animation-iteration-count: infinite; 92 | animation-direction: alternate; 93 | animation-timing-function: linear; 94 | } 95 | @keyframes bounce { 96 | from { bottom: 7.5em } 97 | to { bottom: 6.5em } 98 | } 99 | body > header > div.shadow { 100 | margin-left: -150px; 101 | background-image: url(shadow.svg); 102 | bottom: 4em; 103 | animation-duration: 5s; 104 | animation-name: bounce2; 105 | animation-iteration-count: infinite; 106 | animation-direction: alternate; 107 | animation-timing-function: linear; 108 | } 109 | @keyframes bounce2 { 110 | from { margin-left: -150px } 111 | to { margin-left: -155px } 112 | } 113 | body > header > div ul { 114 | list-style: none; 115 | margin: 0 auto; 116 | padding: 0; 117 | max-width: 15em; 118 | } 119 | 120 | body > article { 121 | padding: 3em 0; 122 | margin: 0 1.5em; 123 | border-bottom: 1px dotted #71746C; 124 | font-family: serif; 125 | text-align: center; 126 | } 127 | body > article ul { 128 | text-align: left; 129 | font-size: 85%; 130 | padding: .5em 0; 131 | margin: 1em auto; 132 | font-family: sans-serif; 133 | font-weight: lighter; 134 | list-style: none; 135 | border-top: 1px dotted #71746C; 136 | border-bottom: 1px dotted #71746C; 137 | max-width: 28em; 138 | } 139 | body > article ul li { 140 | margin-bottom: .3em; 141 | } 142 | body > article li strong { 143 | font-weight: normal; 144 | } 145 | body > article > p { 146 | margin: 0 auto 1em; 147 | max-width: 24em; 148 | text-align: justify; 149 | } 150 | body > article > img { 151 | margin: 0 auto 1.5em; 152 | } 153 | body > article > pre { 154 | margin: 1em auto; 155 | max-width: 26em; 156 | text-align: left; 157 | padding: .5em 1em; 158 | } 159 | 160 | body > footer { 161 | text-align: center; 162 | padding: 2em 0 3em; 163 | } 164 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Felt: On-demand bundler for ES6 / CSS Next 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Felt

14 |

a simple web server
15 | with the power of the future

16 |
17 |

Write

18 |
    19 |
  • in ES6/7
  • 20 |
  • in CSS Next
  • 21 |
22 |
23 |
24 |

Serve

25 |
    26 |
  • in ES5
  • 27 |
  • in Native CSS
  • 28 |
29 |
30 |
31 |
32 |
33 | 34 |
35 |

No more pre-compile

36 |

These days, we have to do so many things before publishing the code. 37 | Babel, JSX, Sass and whatelse, they take extra steps and our time. 38 | Felt lets us be free from such messy things. 39 | It does all kinds of conversions of the front-end codes, for you. 40 | Forget the past, focus on the future!

41 |
42 | $ npm i -g felt felt-recipe-minimal
43 | $ cd path/to/project
44 | $ felt
45 | 
46 |

See more details at our repo on GitHub.

47 |
48 | 49 |
50 | 51 |

How it works

52 |

Behind the scenes, awesome stuff works together: Express, Rollup and PostCSS. 53 | Express is the de facto web server on Node. 54 | Rollup brings the speed to compilation for JavaScript. 55 | PostCSS translate futuristic codes into compatible CSS.

56 |

But the important thing is that we don't have to care about these technologies behind Felt. 57 | We provide "recipes" which are the combination of tools and the settings.

58 |
59 | 60 |
61 | 62 |

Recipes of the future

63 |

Felt runs right out of the box with its "recipes". Basic recipes cover 80% of the cases. On the other hand, we know a lot of special cases exist, too. So we have the mergeable config mechanism in it.

64 | 69 |

A recipe provides the information: which plugins we should use in PostCSS, Rollup and so on. We can overwrite some details with felt.config.js or via arguments. It means that you don't have to care about the detail, and also you have full controll of the configuration.

70 |
71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cognitom/felt/e97218aef9876ebdb2e6047138afe64ed37a4c83/doc/logo.png -------------------------------------------------------------------------------- /doc/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /doc/noize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cognitom/felt/e97218aef9876ebdb2e6047138afe64ed37a4c83/doc/noize.png -------------------------------------------------------------------------------- /doc/priority.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | priority 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | recipe 26 | 27 | 28 | 29 | 30 | 31 | 32 | config 33 | 34 | 35 | 36 | 37 | 38 | 39 | options 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /doc/shadow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | shadow 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /doc/system.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | system 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Rollup 17 | 18 | 19 | Express 20 | 21 | 22 | cache 23 | 24 | 25 | PostCSS 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const 4 | co = require('co'), 5 | log = require('loglevel'), 6 | refresh = require('./lib/refresh'), 7 | watch = require('./lib/watch'), 8 | handler = require('./lib/handler'), 9 | configBuilder = require('./lib/config-builder') 10 | 11 | module.exports = function(...configs) { 12 | const opts = configBuilder(...configs) 13 | 14 | const loglevel = opts.debug ? log.levels.DEBUG : log.levels.ERROR 15 | log.setLevel(loglevel, false) 16 | 17 | co(function* () { 18 | if (opts.refresh) { 19 | yield refresh(opts) 20 | log.debug('Refreshing completed!') 21 | } 22 | if (opts.watch) { 23 | watch(opts) 24 | log.debug('Watching started!') 25 | } 26 | }) 27 | 28 | return handler(opts) 29 | } 30 | -------------------------------------------------------------------------------- /lib/config-builder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | /** default values */ 6 | const defaults = { 7 | src: '.', 8 | cache: 'cache', 9 | root: process.cwd(), 10 | handlers: {}, // default handlers for each extension 11 | patterns: [], // array of globs to handle 12 | external: {}, 13 | update: 'once', // 'never', 'once' or 'always' 14 | refresh: true, // refresh on starting 15 | watch: false, 16 | maxAge: 0, 17 | debug: false, 18 | excludes: ['node_modules/**'] 19 | } 20 | 21 | module.exports = function(...configs) { 22 | const opts = Object.assign({}, defaults) 23 | 24 | // composition of recipes, configs and overwrites 25 | for (const config of configs) { 26 | config.handlers = Object.assign(opts.handlers, config.handlers || {}) 27 | config.external = Object.assign(opts.external, config.external || {}) 28 | config.excludes = opts.excludes.concat(config.excludes || []) 29 | Object.assign(opts, config) 30 | } 31 | 32 | // some checks and modifications 33 | if (opts.src == opts.cache || isInsideDir(opts.src, opts.cache)) 34 | throw new Error('The src directory needs to be outside the cache') 35 | const cacheFromSrc = isInsideDir(opts.cache, opts.src) 36 | if (cacheFromSrc) opts.excludes.push(`${ cacheFromSrc }/**`) 37 | if (!opts.patterns.length) 38 | opts.patterns = Object.keys(opts.handlers).map(ext => `**/*${ ext }`) 39 | 40 | // wires up the patterns and handlers 41 | opts.patterns = opts.patterns.map(entry => { 42 | if (typeof entry == 'string') entry = { pattern: entry } 43 | if (!entry.pattern) throw new Error('No pattern') 44 | if (entry.handler) return entry 45 | for (const ext in opts.handlers) 46 | if (new RegExp(`\\${ ext }$`).test(entry.pattern)) 47 | return Object.assign(entry, { handler: opts.handlers[ext] }) 48 | throw new Error(`No handler registered: ${ entry.pattern }`) 49 | }) 50 | 51 | return opts 52 | } 53 | 54 | /** 55 | * If dir is inside targetDir, return the relative path 56 | * If else, return false 57 | */ 58 | function isInsideDir(dir, targetDir) { 59 | if (targetDir == dir) return false 60 | if (targetDir == '.') return dir 61 | const relative = path.relative(targetDir, dir) 62 | if (/^\.\./.test(relative)) return false 63 | return relative 64 | } 65 | -------------------------------------------------------------------------------- /lib/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const 4 | fsp = require('fs-promise'), 5 | co = require('co'), 6 | url = require('url'), 7 | path = require('path'), 8 | log = require('loglevel'), 9 | minimatch = require('minimatch'), 10 | mkdirs = require('./mkdirs') 11 | 12 | /** 13 | * serves a cache file 14 | */ 15 | function serve(res, file, maxAge) { 16 | log.debug(`Serving: ${ file }`) 17 | res.status(200) 18 | .set('Cache-Control', `max-age=${ maxAge }`) 19 | .sendFile(file, err => { 20 | if (err) { 21 | log.error(err) 22 | res.status(err.status).end() 23 | return 24 | } 25 | }) 26 | } 27 | 28 | /** 29 | * finds a handler matched with pathname 30 | */ 31 | function find(pathname, patterns) { 32 | for (const entry of patterns) 33 | if (minimatch(pathname, entry.pattern)) 34 | return entry.handler 35 | return null 36 | } 37 | 38 | module.exports = function(opts) { 39 | return co.wrap(function* (req, res, next) { 40 | const 41 | pathname = url.parse(req.url).pathname.slice(1), 42 | src = path.join(opts.root, opts.src, pathname), 43 | cache = path.join(opts.root, opts.cache, pathname) 44 | 45 | log.debug(`Request: ${ pathname }`) 46 | 47 | if (req.method != 'GET' && req.method != 'HEAD') return next() 48 | 49 | for (const pattern of opts.excludes) 50 | if (minimatch(pathname, pattern)) 51 | return 52 | 53 | for (const ref in opts.external) 54 | if (ref == pathname) { 55 | serve(res, cache, opts.maxAge) 56 | return 57 | } 58 | 59 | if (/\.map$/.test(pathname)) { 60 | try { 61 | yield fsp.access(cache, fsp.F_OK) 62 | serve(res, cache, opts.maxAge) 63 | return 64 | } catch (err) { 65 | return next() 66 | } 67 | } 68 | 69 | const handler = find(pathname, opts.patterns) 70 | if (!handler) return next() 71 | 72 | try { 73 | yield fsp.access(src, fsp.F_OK) 74 | log.debug(`Source found: ${ src }`) 75 | } catch (err) { 76 | return next() 77 | } 78 | 79 | if (opts.update == 'always') { 80 | yield mkdirs(path.dirname(cache)) 81 | log.debug(`Compiling: ${ pathname }`) 82 | yield handler(src, cache) 83 | } else { 84 | try { 85 | yield fsp.access(cache, fsp.F_OK) 86 | log.debug(`Cache found: ${ cache }`) 87 | } catch (err) { 88 | if (opts.update == 'never') { 89 | log.error('No compiled file found') 90 | return 91 | } 92 | yield mkdirs(path.dirname(cache)) 93 | log.debug(`Compiling: ${ pathname }`) 94 | yield handler(src, cache) 95 | } 96 | } 97 | 98 | serve(res, cache, opts.maxAge) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /lib/mkdirs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const 4 | fsp = require('fs-promise'), 5 | co = require('co'), 6 | log = require('loglevel'), 7 | del = require('del') 8 | 9 | module.exports = co.wrap(function* (dir) { 10 | try { 11 | const stats = yield fsp.stat(dir) 12 | if (stats.isDirectory()) return 13 | 14 | log.debug(`Deleting: ${ dir } (because it's not a directory)`) 15 | yield del(dir) 16 | } catch (err) { /* */ } 17 | 18 | log.debug(`Creating: ${ dir }`) 19 | yield fsp.mkdirs(dir) 20 | }) 21 | -------------------------------------------------------------------------------- /lib/refresh.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const 4 | co = require('co'), 5 | path = require('path'), 6 | log = require('loglevel'), 7 | cpFile = require('cp-file'), 8 | globOriginal = require('glob'), 9 | promisify = require('tiny-promisify'), 10 | mkdirs = require('./mkdirs') 11 | 12 | const glob = promisify(globOriginal) 13 | 14 | module.exports = co.wrap(function* (opts) { 15 | const 16 | srcDir = path.join(opts.root, opts.src), 17 | cacheDir = path.join(opts.root, opts.cache) 18 | 19 | for (const entry of opts.patterns) { 20 | const 21 | handler = entry.handler, 22 | files = yield glob(entry.pattern, { cwd: srcDir, ignore: opts.excludes }) 23 | 24 | yield Promise.all(files.map(pathname => co(function *(){ 25 | const 26 | from = path.join(srcDir, pathname), 27 | to = path.join(cacheDir, pathname) 28 | 29 | yield mkdirs(path.dirname(to)) 30 | log.debug(`Compiling: ${ pathname }`) 31 | try { 32 | yield handler(from, to) 33 | } catch (err) { 34 | log.error(err) 35 | } 36 | }))) 37 | } 38 | 39 | for (const pathname in opts.external) { 40 | const 41 | from = path.join(opts.root, opts.external[pathname]), 42 | to = path.join(cacheDir, pathname) 43 | 44 | yield mkdirs(path.dirname(to)) 45 | log.debug(`Copying: ${ pathname }`) 46 | try { 47 | yield cpFile(from, to) 48 | } catch (err) { 49 | log.error(err) 50 | } 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const 4 | co = require('co'), 5 | express = require('express'), 6 | log = require('loglevel'), 7 | path = require('path'), 8 | refresh = require('./refresh'), 9 | watch = require('./watch'), 10 | handler = require('./handler') 11 | 12 | module.exports = co.wrap(function* (opts) { 13 | const 14 | app = express(), 15 | loglevel = opts.debug ? log.levels.DEBUG : log.levels.ERROR 16 | 17 | log.setLevel(loglevel, false) 18 | if (opts.refresh) { 19 | yield refresh(opts) 20 | log.debug('Refreshing completed!') 21 | } 22 | if (opts.watch) { 23 | watch(opts) 24 | log.debug('Watching started!') 25 | } 26 | 27 | app.use(handler(opts)) 28 | app.use(express.static(path.join(opts.root, opts.src))) 29 | log.debug(`Listening: ${ opts.port }`) 30 | return app.listen(opts.port) 31 | }) 32 | -------------------------------------------------------------------------------- /lib/static-export.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const 4 | co = require('co'), 5 | path = require('path'), 6 | log = require('loglevel'), 7 | cpy = require('cpy'), 8 | del = require('del'), 9 | refresh = require('./refresh'), 10 | watch = require('./watch'), 11 | mkdirs = require('./mkdirs') 12 | 13 | module.exports = co.wrap(function* (opts) { 14 | const 15 | loglevel = opts.debug ? log.levels.DEBUG : log.levels.ERROR, 16 | src = path.join(opts.root, opts.src), 17 | dest = path.join(opts.root, opts.cache) 18 | 19 | log.setLevel(loglevel, false) 20 | const 21 | patterns = opts.patterns.map(entry => `!${ entry.pattern }`), 22 | globs = ['**', '!**/.*'].concat(patterns) 23 | 24 | yield mkdirs(dest) 25 | yield del('**', { cwd: dest }) 26 | log.debug('All files cleared') 27 | yield cpy(globs, dest, { cwd: src, parents: true, nodir: true, ignore: opts.excludes }) 28 | log.debug('All files copied except the files handled by Felt') 29 | yield refresh(opts) 30 | log.debug(`Exporting completed: ${ opts.cache }`) 31 | 32 | if (opts.watch) { 33 | watch(opts) 34 | log.debug('Watching started!') 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /lib/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const 4 | co = require('co'), 5 | path = require('path'), 6 | log = require('loglevel'), 7 | chokidar = require('chokidar'), 8 | mkdirs = require('./mkdirs') 9 | 10 | module.exports = function(opts) { 11 | const 12 | srcDir = path.join(opts.root, opts.src), 13 | cacheDir = path.join(opts.root, opts.cache) 14 | 15 | for (const entry of opts.patterns) { 16 | const handler = entry.handler 17 | chokidar.watch(entry.pattern, { 18 | cwd: srcDir, 19 | ignoreInitial: true, 20 | ignored: opts.excludes 21 | }).on('all', co.wrap(function* (event, pathname) { 22 | const 23 | from = path.join(srcDir, pathname), 24 | to = path.join(cacheDir, pathname) 25 | 26 | log.debug(`Changed: ${ pathname }`) 27 | yield mkdirs(path.dirname(to)) 28 | log.debug(`Compiling: ${ pathname }`) 29 | yield handler(from, to) 30 | })) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "felt", 3 | "version": "0.3.2", 4 | "description": "Ondemand bundler for ES6 / CSS Next", 5 | "main": "index.js", 6 | "bin": "./bin/index.js", 7 | "files": [ 8 | "bin", 9 | "lib", 10 | "index.js" 11 | ], 12 | "homepage": "https://github.com/cognitom/felt", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/cognitom/felt.git" 16 | }, 17 | "keywords": [ 18 | "express", 19 | "middleware" 20 | ], 21 | "author": "Tsutomu Kawamura", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/cognitom/felt/issues" 25 | }, 26 | "dependencies": { 27 | "chokidar": "^1.6.1", 28 | "co": "^4.6.0", 29 | "cp-file": "^4.1.0", 30 | "cpy": "^4.0.1", 31 | "del": "^2.2.2", 32 | "express": "^4.14.0", 33 | "fs-promise": "^0.5.0", 34 | "glob": "^7.1.1", 35 | "loglevel": "^1.4.1", 36 | "meow": "^3.7.0", 37 | "minimatch": "^3.0.3", 38 | "request": "^2.75.0", 39 | "tiny-promisify": "^1.0.0" 40 | }, 41 | "devDependencies": { 42 | "ava": "^0.16.0", 43 | "browser-sync": "^2.17.5", 44 | "eslint": "^3.8.1", 45 | "felt-recipe-minimal": "^0.2.0", 46 | "gh-pages": "^0.11.0", 47 | "normalize-css": "^2.3.1" 48 | }, 49 | "scripts": { 50 | "test": "npm run eslint && npm run ava", 51 | "eslint": "eslint index.js lib/*.js bin/*.js", 52 | "ava": "ava test/spec.js", 53 | "start": "npm run server & npm run sync", 54 | "server": "node bin/index.js --src doc --debug --watch --port 3000", 55 | "sync": "browser-sync start --files 'doc/*.html, doc/*.svg, doc/*.png, cache/**' --proxy 'localhost:3000' --port 3030", 56 | "deploy": "npm run export && gh-pages -d dist", 57 | "export": "node bin/index.js --src doc --export dist" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/deeper/ref.js: -------------------------------------------------------------------------------- 1 | Hi! 2 | -------------------------------------------------------------------------------- /test/expect/a.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | function b (message){ 5 | console.log(message) 6 | } 7 | 8 | b('Hello world!') 9 | 10 | }()); 11 | //# sourceMappingURL=a.js.map 12 | -------------------------------------------------------------------------------- /test/expect/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cognitom/felt/e97218aef9876ebdb2e6047138afe64ed37a4c83/test/expect/images/logo.png -------------------------------------------------------------------------------- /test/expect/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | a 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/fixture/a.js: -------------------------------------------------------------------------------- 1 | import b from './b.js' 2 | 3 | b('Hello world!') 4 | -------------------------------------------------------------------------------- /test/fixture/b.js: -------------------------------------------------------------------------------- 1 | export default function b (message){ 2 | console.log(message) 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cognitom/felt/e97218aef9876ebdb2e6047138afe64ed37a4c83/test/fixture/images/logo.png -------------------------------------------------------------------------------- /test/fixture/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | a 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import test from 'ava' 4 | import del from 'del' 5 | import path from 'path' 6 | import fsp from 'fs-promise' 7 | import requestOriginal from 'request' 8 | import promisify from 'tiny-promisify' 9 | import recipeMinimal from 'felt-recipe-minimal' 10 | import staticExport from '../lib/static-export' 11 | import configBuilder from '../lib/config-builder' 12 | import mkdirs from '../lib/mkdirs' 13 | import server from '../lib/server' 14 | 15 | test('builds basic opts from configs', function(t) { 16 | const 17 | config = { src: 'public' }, 18 | opts = configBuilder(config) 19 | 20 | t.is(opts.cache, 'cache') 21 | t.is(opts.root, process.cwd()) 22 | t.is(opts.update, 'once') 23 | t.false(opts.watch) 24 | t.is(opts.maxAge, 0) 25 | t.false(opts.debug) 26 | }) 27 | 28 | test('generates patterns from handlers if not exists', function(t) { 29 | const 30 | obj0 = {}, 31 | obj1 = {}, 32 | cssDummyHandler = () => obj0, 33 | jsDummyHandler = () => obj1, 34 | config = { 35 | src: 'public', 36 | handlers: { 37 | '.css': cssDummyHandler(), 38 | '.js': jsDummyHandler() 39 | } 40 | }, 41 | opts = configBuilder(config) 42 | 43 | t.is(opts.patterns[0].pattern, '**/*.css') 44 | t.is(opts.patterns[0].handler, obj0) 45 | t.is(opts.patterns[1].pattern, '**/*.js') 46 | t.is(opts.patterns[1].handler, obj1) 47 | }) 48 | 49 | test('composes multiple configs', function(t) { 50 | const 51 | config0 = { watch: true }, 52 | config1 = { src: 'public' }, 53 | config2 = { src: 'other' }, // overwrite src 54 | opts = configBuilder(config0, config1, config2) 55 | 56 | t.true(opts.watch) 57 | t.is(opts.src, 'other') 58 | }) 59 | 60 | test('cache and src is the same', function(t) { 61 | try { 62 | const 63 | config = { src: 'public', cache: 'public' }, 64 | opts = configBuilder(config) 65 | 66 | t.fail() 67 | } catch (err) { 68 | t.pass() 69 | } 70 | }) 71 | 72 | test('src is inside of cache', function(t) { 73 | try { 74 | const 75 | config = { src: 'cache/a', cache: 'cache' }, 76 | opts = configBuilder(config) 77 | 78 | t.fail() 79 | } catch (err) { 80 | t.pass() 81 | } 82 | }) 83 | 84 | test('cache is inside of src', function(t) { 85 | const 86 | config = { src: 'public', cache: 'public/cache' }, 87 | opts = configBuilder(config) 88 | 89 | t.truthy(~opts.excludes.indexOf('cache/**')) 90 | }) 91 | 92 | test('src is dot(.)', function(t) { 93 | const 94 | config = { src: '.', cache: 'cache' }, 95 | opts = configBuilder(config) 96 | 97 | t.truthy(~opts.excludes.indexOf('cache/**')) 98 | }) 99 | 100 | test('makes dir at empty', async function(t) { 101 | const dir = path.join(__dirname, 'mkdirs-test') 102 | 103 | await mkdirs(dir) 104 | 105 | let dirCreated = false 106 | try { 107 | const stats = await fsp.stat(dir) 108 | if (stats.isDirectory()) dirCreated = true 109 | } catch (err) { /* */ } 110 | 111 | t.true(dirCreated) 112 | 113 | await del(dir) 114 | }) 115 | 116 | test('makes dir but already file exists', async function(t) { 117 | const dir = path.join(__dirname, 'mkdirs-test2') 118 | 119 | await fsp.writeFile(dir, 'Hi!') 120 | await mkdirs(dir) 121 | 122 | let dirCreated = false 123 | try { 124 | const stats = await fsp.stat(dir) 125 | if (stats.isDirectory()) dirCreated = true 126 | } catch (err) { /* */ } 127 | 128 | t.true(dirCreated) 129 | 130 | await del(dir) 131 | }) 132 | 133 | test('bundles scripts', async function(t) { 134 | const 135 | port = 3333, 136 | opts = configBuilder(recipeMinimal, { src: 'fixture', port }), 137 | url = `http://localhost:${ port }/a.js`, 138 | myServer = await server(opts), 139 | actual = await request(url), 140 | expected = await readFile('expect/a.js') 141 | 142 | t.is(actual, expected) 143 | myServer.close() 144 | }) 145 | 146 | test('serves static contents', async function(t) { 147 | const 148 | port = 3334, 149 | opts = configBuilder(recipeMinimal, { src: 'fixture', port }), 150 | url = `http://localhost:${ port }/index.html`, 151 | myServer = await server(opts), 152 | actual = await request(url), 153 | expected = await readFile('expect/index.html') 154 | 155 | t.is(actual, expected) 156 | myServer.close() 157 | }) 158 | 159 | test('serves refs', async function(t) { 160 | const 161 | port = 3335, 162 | opts = configBuilder(recipeMinimal, { 163 | src: 'fixture', 164 | port, 165 | external: { 166 | 'my-ref.js': 'deeper/ref.js' 167 | } 168 | }), 169 | url = `http://localhost:${ port }/my-ref.js`, 170 | myServer = await server(opts), 171 | actual = await request(url), 172 | expected = 'Hi!' 173 | 174 | t.is(actual, expected) 175 | myServer.close() 176 | }) 177 | 178 | test('serves static contents in sub directory', async function(t) { 179 | const 180 | port = 3336, 181 | opts = configBuilder(recipeMinimal, { src: 'fixture', port }), 182 | url = `http://localhost:${ port }/images/logo.png`, 183 | myServer = await server(opts), 184 | actual = await request(url), 185 | expected = await readFile('expect/images/logo.png') 186 | 187 | t.is(actual, expected) 188 | myServer.close() 189 | }) 190 | 191 | test('export bundles scripts', async function(t) { 192 | const 193 | dir = path.join(__dirname, 'dist'), 194 | opts = configBuilder(recipeMinimal, { src: 'fixture', cache: 'dist' }) 195 | await staticExport(opts) 196 | const 197 | actual = await readFile('dist/a.js'), 198 | expected = await readFile('expect/a.js') 199 | 200 | t.is(actual, expected) 201 | await del(dir) 202 | }) 203 | 204 | test('export static contents', async function(t) { 205 | const 206 | dir = path.join(__dirname, 'dist'), 207 | opts = configBuilder(recipeMinimal, { src: 'fixture', cache: 'dist' }) 208 | await staticExport(opts) 209 | const 210 | actual = await readFile('dist/index.html'), 211 | expected = await readFile('expect/index.html') 212 | 213 | t.is(actual, expected) 214 | await del(dir) 215 | }) 216 | 217 | test('export refs', async function(t) { 218 | const 219 | dir = path.join(__dirname, 'dist'), 220 | opts = configBuilder(recipeMinimal, { src: 'fixture', cache: 'dist' }) 221 | await staticExport(opts) 222 | const 223 | actual = await readFile('dist/my-ref.js'), 224 | expected = await readFile('deeper/ref.js') 225 | 226 | t.is(actual, expected) 227 | await del(dir) 228 | }) 229 | 230 | test('export static contents in sub directory', async function(t) { 231 | const 232 | dir = path.join(__dirname, 'dist'), 233 | opts = configBuilder(recipeMinimal, { src: 'fixture', cache: 'dist' }) 234 | await staticExport(opts) 235 | const 236 | actual = await readFile('dist/images/logo.png'), 237 | expected = await readFile('expect/images/logo.png') 238 | 239 | t.is(actual, expected) 240 | await del(dir) 241 | }) 242 | 243 | function removeLastNewLine(str) { 244 | return str.replace(/\n$/, '') 245 | } 246 | 247 | async function readFile(file) { 248 | const content = await fsp.readFile(file, 'utf8') 249 | return removeLastNewLine(content) 250 | } 251 | 252 | async function request(url) { 253 | const response = await promisify(requestOriginal)(url) 254 | return removeLastNewLine(response.body) 255 | } 256 | --------------------------------------------------------------------------------