├── .gitignore ├── .npmignore ├── History.md ├── Makefile ├── Readme.md ├── example └── react │ ├── base.css │ ├── bundle.js │ ├── client.css │ ├── client.html │ ├── client.js │ ├── external.js │ ├── fonts │ └── Roboto-Regular.ttf │ ├── images │ └── img.JPG │ └── index.js ├── index.js ├── package.json ├── test.js └── test ├── bundle.js └── fixtures ├── mount └── mount.js └── simple.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 2.0.0 / 2015-10-01 3 | ================== 4 | 5 | * BREAKING: route now is request path, not fullpath. 6 | 7 | 1.0.14 / 2015-09-30 8 | ================== 9 | 10 | * add the ability to pass file-specific parameters to the builder 11 | 12 | 1.0.13 / 2015-08-14 13 | ================== 14 | 15 | * prevent middleware from continuing on source maps and messing things up 16 | 17 | 1.0.12 / 2015-07-22 18 | ================== 19 | 20 | * fix for fonts 21 | 22 | 1.0.11 / 2015-07-20 23 | ================== 24 | 25 | * fix koa-bundle for http assets 26 | 27 | 1.0.10 / 2015-07-20 28 | ================== 29 | 30 | * add seamless support for dependencies within stylesheets 31 | 32 | 1.0.9 / 2015-07-19 33 | ================== 34 | 35 | * pass CSS errors to the frontend 36 | 37 | 1.0.8 / 2015-06-12 38 | ================== 39 | 40 | * escape HTML in error message 41 | 42 | 1.0.7 / 2015-06-09 43 | ================== 44 | 45 | * pass server-side errors to the client in dev 46 | 47 | 1.0.6 / 2015-06-05 48 | ================== 49 | 50 | * fix 1 argument 51 | 52 | 1.0.5 / 2015-06-05 53 | ================== 54 | 55 | * fix when there is no src 56 | * Release 1.0.4 57 | * fix up routing path and allow you to pass options to middleware 58 | * still display error when no stack is present 59 | 60 | 1.0.4 / 2015-06-05 61 | ================== 62 | 63 | * fix up routing path and allow you to pass options to middleware 64 | 65 | 1.0.3 / 2015-05-31 66 | ================== 67 | 68 | * fix routing 69 | 70 | 1.0.2 / 2015-05-31 71 | ================== 72 | 73 | * cleanup 74 | 75 | 1.0.1 / 2015-05-31 76 | ================== 77 | 78 | * Actually merge the branch 79 | * new API, lots of bug fixes 80 | 81 | 1.0.0 / 2015-05-31 82 | ================== 83 | 84 | * New API to be route agnostic across mounts 85 | * Bugfixes, testing and a complete example 86 | 87 | 0.1.3 / 2015-04-10 88 | ================== 89 | 90 | * support passing the file object back through 91 | 92 | 0.1.2 / 2015-04-10 93 | ================== 94 | 95 | * better support when sourcemap is not present, thanks to @dominicbarnes & @thlorenz! 96 | 97 | 0.1.1 / 2015-03-11 98 | ================== 99 | 100 | * pass the context through and more transparent routing via DEBUG 101 | * fix example 102 | 103 | 0.1.0 / 2015-03-08 104 | ================== 105 | 106 | * fix routing for node_modules 107 | 108 | 0.0.4 / 2015-03-08 109 | ================== 110 | 111 | * fix for node_modules 112 | 113 | 0.0.3 / 2015-03-08 114 | ================== 115 | 116 | * better extension handling 117 | 118 | 0.0.2 / 2015-03-07 119 | ================== 120 | 121 | * support options with currying 122 | 123 | 0.0.1 / 2015-03-07 124 | ================== 125 | 126 | * Initial commit 127 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | example: 2 | 3 | test: 4 | @./node_modules/.bin/mocha \ 5 | --require should \ 6 | --reporter spec 7 | 8 | .PHONY: test example 9 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # koa-bundle 3 | 4 | Generic asset pipeline with caching, etags, minification, gzipping and sourcemaps. 5 | 6 | The child of [node-enchilada](https://github.com/defunctzombie/node-enchilada) and [static-cache](https://github.com/koajs/static-cache). 7 | 8 | ## Examples 9 | 10 | - Browserify (with a callback and options) 11 | 12 | ```js 13 | var bundle = Bundle({ debug: true, root: __dirname) }, function(file, fn) { 14 | Browserify({ debug: file.debug }) 15 | .add(file.path) 16 | .transform(require('babelify')) 17 | .bundle(fn); 18 | })) 19 | app.use(bundle('app.js')); 20 | ``` 21 | 22 | - Duo (using generators) 23 | 24 | ```js 25 | var bundle = Bundle(function *(file) { 26 | return yield Duo(file.root) 27 | .entry(file.path) 28 | .use(require('duo-sass')()) 29 | .run(); 30 | }) 31 | app.use(bundle('app.css')); 32 | ``` 33 | 34 | - Gulp (using currying and globbing) 35 | 36 | ```js 37 | var bundle = Bundler({ root: __dirname }, function(file, fn) { 38 | var gulp = Gulp.src(file.path, { cwd: file.root }); 39 | 40 | if ('styl' == file.type) { 41 | gulp.pipe(styl()) 42 | .on('error', fn); 43 | } 44 | 45 | gulp.pipe(myth()) 46 | .on('error', fn) 47 | 48 | if ('production' == process.env.NODE_ENV) { 49 | gulp 50 | .pipe(csso()) 51 | .on('error', fn); 52 | } 53 | 54 | gulp.on('end', fn); 55 | }); 56 | 57 | // ... in another file, single middleware 58 | app.use(bundle()); 59 | 60 | // multiple endpoints 61 | bundle('app.styl'); 62 | bundle('app.js'); 63 | ``` 64 | 65 | ## Installation 66 | 67 | ```js 68 | npm install koa-bundle 69 | ``` 70 | 71 | ## API 72 | 73 | #### `bundle(settings, handler) => bundler([path]) => middleware` 74 | #### `bundle(handler)(glob) => bundler([path]) => middleware` 75 | 76 | Create a bundler with an optional set of `settings` and a `handler`. 77 | 78 | A `handler` can be a synchronous function, asynchronous function, generator or promise. The handler passes a `File` object that has the following properties: 79 | 80 | ```js 81 | var File = { 82 | type: "js", 83 | src: "... JS ...", 84 | path: "dashboard.js", 85 | root: "/Users/Matt/Projects/..." 86 | minify: true, 87 | debug: false, 88 | cache: true, 89 | gzip: true, 90 | } 91 | ``` 92 | 93 | --- 94 | 95 | The available `settings` are: 96 | 97 | - `debug`: enables sourcemaps 98 | - `minify`: minify JS and CSS 99 | - `cache`: cache responses across requests and add etags 100 | - `gzip`: gzip the response if it's supported 101 | 102 | The default settings depend on the environment (`NODE_ENV`): 103 | 104 | - Production: 105 | 106 | - `debug`: false 107 | - `minify`: true 108 | - `cache`: true 109 | - `gzip`: true 110 | 111 | - Development: 112 | 113 | - `debug`: true 114 | - `minify`: false 115 | - `cache`: false 116 | - `gzip`: false 117 | 118 | --- 119 | 120 | The bundler returns a function that you can then pass a `path` into: 121 | 122 | ```js 123 | var bundle = Bundler(settings, handler); 124 | app.use(bundle('app.js')); 125 | ``` 126 | 127 | The `path` is relative to `settings.root` or `process.cwd()`. The `script[src]` and `link[href]` is relative the `root` specified. 128 | 129 | ## TODO 130 | 131 | - Warmup cache in production 132 | - More examples 133 | - Testing 134 | 135 | ## Credits 136 | 137 | - [node-enchilada](https://github.com/defunctzombie/node-enchilada) and [browserify-middleware](https://github.com/forbeslindesay/browserify-middleware) for some ideas and general design. 138 | - [static-cache](https://github.com/koajs/static-cache) for the caching, etagging and gzipping. 139 | - sponsored by [Lapwing Labs](http://lapwinglabs.com). 140 | 141 | ## License 142 | 143 | MIT 144 | 145 | Copyright (c) 2015 Matthew Mueller <matt@lapwinglabs.com> 146 | -------------------------------------------------------------------------------- /example/react/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: red; 3 | } 4 | -------------------------------------------------------------------------------- /example/react/bundle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var Browserify = require('browserify'); 6 | var resolve = require('path').resolve; 7 | var join = require('path').join; 8 | var npm = require('rework-npm'); 9 | var rework = require('rework'); 10 | var Bundle = require('../../'); 11 | var myth = require('myth'); 12 | var fs = require('fs'); 13 | 14 | /** 15 | * Transforms 16 | */ 17 | 18 | var babelify = require('babelify'); 19 | var envify = require('envify'); 20 | 21 | /** 22 | * $NODE_PATH 23 | */ 24 | 25 | var nodePath = join(__dirname, '..', '..'); 26 | 27 | /** 28 | * Export `bundle` 29 | */ 30 | 31 | module.exports = Bundle({ root: __dirname }, function(file, fn) { 32 | var options = { 33 | extensions: ['.jsx'], 34 | debug: file.debug, 35 | paths: nodePath 36 | } 37 | 38 | if ('jsx' == file.type) { 39 | file.type = 'js'; 40 | } 41 | 42 | if ('js' == file.type) { 43 | Browserify(options) 44 | .on('error', fn) 45 | .external(['react']) 46 | .add(file.path) 47 | .transform(babelify) 48 | .transform(envify) 49 | .bundle(fn); 50 | } else if ('css' == file.type) { 51 | fs.readFile(file.path, 'utf8', function(err, str) { 52 | if (err) return fn(err); 53 | 54 | try { 55 | var css = rework(str, { source: file.path }) 56 | .use(npm({ root: join(__dirname, '..', '..') })) 57 | .use(myth()) 58 | .toString({ sourcemap: !!file.debug }); 59 | } catch (e) { 60 | return fn(e); 61 | } 62 | 63 | fn(null, css); 64 | }); 65 | } else { 66 | fs.readFile(file.path, fn); 67 | } 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /example/react/client.css: -------------------------------------------------------------------------------- 1 | @import "./base.css"; 2 | 3 | @font-face { 4 | font-family: 'Roboto'; 5 | font-weight: 300; 6 | font-style: normal; 7 | src: local("☺"), 8 | url('fonts/Roboto-Regular.ttf') format('truetype'); 9 | } 10 | 11 | body { 12 | background-image: url('./images/img.JPG'); 13 | } 14 | 15 | h2 { 16 | color: purple; 17 | font-family: 'Roboto'; 18 | } 19 | -------------------------------------------------------------------------------- /example/react/client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Example 5 | 6 | 7 | 8 |

hi

9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/react/client.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | console.log(React); 3 | console.log('hello!!!!!!'); 4 | -------------------------------------------------------------------------------- /example/react/external.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var Browserify = require('browserify'); 6 | var basename = require('path').basename; 7 | var extname = require('path').extname; 8 | var resolve = require('path').resolve; 9 | var Bundle = require('../..'); 10 | 11 | /** 12 | * Export `bundle` 13 | */ 14 | 15 | module.exports = Bundle({ root: __dirname, requires: ['react'] }, function(file, fn) { 16 | var path = file.path; 17 | var mod = file.mod; 18 | 19 | var options = { 20 | debug: file.debug, 21 | exposeAll: true, 22 | noparse: true 23 | } 24 | 25 | Browserify(options) 26 | .on('error', fn) 27 | .require(file.path, { expose: mod, basedir: file.root }) 28 | .bundle(fn); 29 | }); 30 | -------------------------------------------------------------------------------- /example/react/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koajs/bundle/e93b13fb3693883c632cc1dd7641366635d62ab2/example/react/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /example/react/images/img.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koajs/bundle/e93b13fb3693883c632cc1dd7641366635d62ab2/example/react/images/img.JPG -------------------------------------------------------------------------------- /example/react/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var external = require('./external.js'); 6 | var bundle = require('./bundle.js'); 7 | var roo = require('roo')(__dirname); 8 | 9 | roo.use(external('react')); 10 | roo.use(bundle({ root: __dirname })); 11 | 12 | bundle(__dirname + '/client.js?external'); 13 | bundle(__dirname + '/client.css'); 14 | 15 | roo.get('/', 'client.html'); 16 | 17 | roo.listen(5050, function() { 18 | var addr = this.address(); 19 | console.log('listening on [%s]:%s', addr.address, addr.port); 20 | }) 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var browser_resolve = require('browser-resolve').sync; 6 | var convert = require('convert-source-map'); 7 | var debug = require('debug')('koa-bundle'); 8 | var compressible = require('compressible'); 9 | var normalize = require('path').normalize; 10 | var basename = require('path').basename; 11 | var relative = require('path').relative; 12 | var escapeHTML = require('escape-html'); 13 | var read = require('fs').readFileSync; 14 | var exists = require('fs').existsSync; 15 | var extname = require('path').extname; 16 | var resolve = require('path').resolve; 17 | var assign = require('object-assign'); 18 | var filedeps = require('file-deps'); 19 | var uglify = require('uglify-js'); 20 | var toHTML = require('ansi-html'); 21 | var mime = require('mime-types'); 22 | var join = require('path').join; 23 | var wrapfn = require('wrap-fn'); 24 | var qs = require('querystring'); 25 | var isBuffer = Buffer.isBuffer; 26 | var crypto = require('crypto'); 27 | var sep = require('path').sep; 28 | var zlib = require('zlib'); 29 | var csso = require('csso'); 30 | var url = require('url'); 31 | var cwd = process.cwd(); 32 | var fs = require('fs'); 33 | 34 | /** 35 | * Export `bundle` 36 | */ 37 | 38 | module.exports = bundle; 39 | 40 | /** 41 | * Is production? 42 | */ 43 | 44 | var production = 'production' == process.env.NODE_ENV; 45 | 46 | /** 47 | * Default settings 48 | */ 49 | 50 | var defaults = production 51 | ? { 52 | debug: false, 53 | minify: true, 54 | cache: true, 55 | gzip: true 56 | } 57 | : { 58 | debug: true, 59 | minify: false, 60 | cache: false, 61 | gzip: false 62 | } 63 | ; 64 | 65 | /** 66 | * Initialize `bundle` 67 | * 68 | * @param {String} path 69 | * @param {Object} options 70 | * @param {Function|Generator} fn 71 | */ 72 | 73 | function bundle(settings, fn) { 74 | if (arguments.length == 1) fn = settings, settings = {}; 75 | settings = assign(defaults, settings); 76 | var root = settings.root || cwd; 77 | var entries = {}; 78 | 79 | return function _bundle(settings2, path) { 80 | if (arguments.length == 2) { 81 | settings = assign(settings, settings2); 82 | root = settings.root; 83 | var obj = entry(root, path); 84 | entries[join(root, obj.route)] = obj; 85 | } else if (arguments.length == 1) { 86 | if ('string' == typeof settings2) { 87 | root = settings.root; 88 | var obj = entry(root, settings2); 89 | entries[join(root, obj.route)] = obj; 90 | } else { 91 | settings = assign(settings, settings2); 92 | } 93 | } 94 | 95 | return middleware.call(this, entries, settings, fn); 96 | } 97 | } 98 | 99 | /** 100 | * Add an entry 101 | * 102 | * @param {String} root 103 | * @param {String} path 104 | * @param {Object} options 105 | * @param {Object} 106 | */ 107 | 108 | function entry(root, mod, options) { 109 | var obj = url.parse(mod) 110 | mod = obj.pathname 111 | 112 | var node_module = nm(root, mod); 113 | var route; 114 | var path; 115 | 116 | if (node_module) { 117 | route = relative(root, resolve(root, normalize(mod))); 118 | path = node_module; 119 | } else { 120 | path = fullpath(root, mod, options); 121 | if (path instanceof Error) return path; 122 | route = '/' + relative(root, path); 123 | } 124 | 125 | debug('GET /%s => %s', route, path); 126 | 127 | var type = extname(path).slice(1); 128 | 129 | return assign({ 130 | route: route, 131 | mtime: null, 132 | type: type, 133 | path: path, 134 | md5: null, 135 | mod: mod, 136 | size: 0 137 | }, qs.parse(obj.query)); 138 | } 139 | 140 | /** 141 | * Create the middleware 142 | * 143 | * @param {Array} entries 144 | * @param {Object} settings 145 | * @param {Function|Generator} fn 146 | */ 147 | 148 | function middleware(entries, settings, fn) { 149 | var root = settings.root = settings.root || cwd; 150 | var ctx = this; 151 | var maps = {}; 152 | 153 | return function *bundle(next) { 154 | // only accept HEAD and GET 155 | if (this.method !== 'HEAD' && this.method !== 'GET') return yield* next; 156 | 157 | // decode for `/%E4%B8%AD%E6%96%87` 158 | // normalize for `//index` 159 | var path = join(root, decode(normalize(this.path))); 160 | 161 | if (settings.debug && maps[path]) { 162 | debug('fetching sourcemap: %s', path); 163 | return this.body = maps[path]; 164 | } else if (!entries[path]) { 165 | return yield* next; 166 | } 167 | 168 | var file = entries[path]; 169 | var encodings = this.acceptsEncodings(); 170 | if (settings.cache && file.md5) { 171 | debug('asset cached') 172 | // HACK: set status to 2xx make sure that fresh gets called 173 | this.status = 200; 174 | this.response.etag = file.md5; 175 | 176 | // don't send anything for repeat clients 177 | if (this.fresh) { 178 | debug('asset still fresh, returning 304'); 179 | return this.status = 304; 180 | } 181 | 182 | if (settings.gzip && file.zip && shouldGzip(file, encodings)) { 183 | debug('serving cached gzipped asset'); 184 | this.remove('Content-Length'); 185 | this.set('Content-Encoding', 'gzip'); 186 | this.type = file.type; 187 | return this.body = file.zip; 188 | } else if (file.src) { 189 | debug('serving cached asset'); 190 | this.type = file.type; 191 | return this.body = file.src 192 | } 193 | } 194 | 195 | debug('building the asset'); 196 | try { 197 | var src = yield function(done) { 198 | wrapfn(fn, done).call(ctx, assign(file, settings)); 199 | } 200 | } catch(e) { 201 | var msg = e.stack ? e.stack : e.toString(); 202 | console.error(msg); 203 | this.status = 500; 204 | 205 | if (!production) { 206 | this.body = 'css' == file.type ? write_css_error(msg) : write_js_error(msg); 207 | this.type = file.type; 208 | this.status = 200; 209 | } 210 | 211 | return; 212 | } 213 | debug('built the asset'); 214 | 215 | if (src && file != src) file.src = src; 216 | this.type = file.type; 217 | 218 | // other types of assets 219 | // TODO: do more intelligent things with results (like etag images, fonts, etc) 220 | if (this.type !== 'application/javascript' && this.type !== 'text/css') { 221 | 222 | // caching the asset 223 | if (settings.cache) { 224 | file.md5 = md5(file.src); 225 | debug('caching the asset md5(%s)', file.md5); 226 | this.response.etag = file.md5; 227 | } 228 | 229 | return this.body = file.src; 230 | } 231 | 232 | // ensure UTF8 for JS and CSS 233 | file.src = file.src.toString() 234 | 235 | // adding in the other dependencies 236 | if (file.type == 'css') { 237 | var deps = filedeps(file.src, file.type); 238 | deps.forEach(function(dep) { 239 | if (http(dep)) return; 240 | dep = stripPath(dep); 241 | var obj = entry(root, dep, { catch: true }); 242 | if (obj instanceof Error) { 243 | return debug('warning: %s', obj.message); 244 | } 245 | debug('added: dependency /%s => %s', obj.route, obj.path) 246 | entries[join(root, obj.route)] = obj; 247 | }) 248 | } 249 | 250 | // generate sourcemaps in debug mode 251 | var srcmap = null; 252 | var mapping = null; 253 | 254 | if (settings.debug) { 255 | debug('building the source map'); 256 | 257 | try { 258 | srcmap = convert.fromComment(file.src); 259 | srcmap = srcmap.sourcemap ? srcmap : false; 260 | file.src = convert.removeComments(file.src); 261 | srcmap.setProperty('file', file.path); 262 | mapping = path.replace(extname(path), '.map.json'); 263 | debug('built the source map'); 264 | } catch (e) { 265 | debug('unable to build the sourcemap: %s', e.toString()); 266 | } 267 | } 268 | 269 | // minify the code 270 | if (settings.minify) { 271 | debug('minifying the asset'); 272 | switch (file.type) { 273 | case 'js': 274 | file.src = compress(file, srcmap); 275 | break; 276 | case 'css': 277 | file.src = csso.justDoIt(file.src); 278 | break; 279 | } 280 | } 281 | 282 | // add in the sourcemap 283 | if (settings.debug && srcmap) { 284 | debug('adding in the source mapping url %s', basename(mapping)); 285 | file.src += '\n//# sourceMappingURL=' + basename(mapping); 286 | maps[mapping] = srcmap.toObject(); 287 | } 288 | 289 | // caching the asset 290 | if (settings.cache) { 291 | file.md5 = md5(file.src); 292 | debug('caching the asset md5(%s)', file.md5); 293 | this.response.etag = file.md5; 294 | } 295 | 296 | // finally calculate the file size 297 | file.size = file.src ? file.src.length : 0; 298 | 299 | // gzip the asset or serve it directly 300 | if (settings.gzip && shouldGzip(file, encodings)) { 301 | debug('serving the gzipped asset'); 302 | this.remove('Content-Length'); 303 | this.set('Content-Encoding', 'gzip'); 304 | this.type = file.type; 305 | file.zip = yield gzip(file.src); 306 | this.body = file.zip; 307 | } else { 308 | debug('serving the asset'); 309 | this.type = file.type; 310 | this.body = file.src; 311 | } 312 | } 313 | } 314 | 315 | /** 316 | * Gzip the file 317 | * 318 | * @param {String} src 319 | * @return {Buffer} 320 | */ 321 | 322 | function gzip(src) { 323 | var buf = new Buffer(src); 324 | return function (done) { 325 | zlib.gzip(buf, done) 326 | } 327 | } 328 | 329 | /** 330 | * Should we gzip? 331 | * 332 | * @param {Object} file 333 | * @param {Array} encodings 334 | * @return {Boolean} 335 | */ 336 | 337 | function shouldGzip(file, encodings) { 338 | return file.size > 1024 339 | && ~encodings.indexOf('gzip') 340 | && compressible(mime.lookup(file.type)); 341 | } 342 | 343 | /** 344 | * Compress the file and 345 | * recalculate sourcemaps 346 | * 347 | * @param {Object} file 348 | * @param {Object} srcmap 349 | * @return {String} 350 | */ 351 | 352 | function compress(file, srcmap) { 353 | var opts = { 354 | fromString: true 355 | }; 356 | 357 | if (srcmap) { 358 | opts.inSourceMap = srcmap.toObject(); 359 | opts.outSourceMap = basename(file.path); 360 | } 361 | 362 | var src = file.src; 363 | var result = uglify.minify(src, opts); 364 | 365 | if (srcmap) { 366 | // prepare new sourcemap 367 | // we need to get the sources from bundled sources 368 | // uglify does not carry those through 369 | var srcs = srcmap.getProperty('sourcesContent'); 370 | srcmap = convert.fromJSON(result.map); 371 | srcmap.setProperty('sourcesContent', srcs); 372 | } 373 | 374 | return result.code; 375 | } 376 | 377 | /** 378 | * Safely resolve a node_module 379 | * 380 | * @param {String} mod 381 | * @return {Boolean|String} mod 382 | */ 383 | 384 | function nm(root, entry) { 385 | try { 386 | return browser_resolve(entry, { basedir: root }); 387 | } catch (e) { 388 | return false; 389 | } 390 | } 391 | 392 | /** 393 | * Calculate the MD5 394 | * 395 | * @param {String} src 396 | * @param {String} md5 397 | */ 398 | 399 | function md5(src) { 400 | return crypto 401 | .createHash('md5') 402 | .update(src) 403 | .digest('base64'); 404 | } 405 | 406 | /** 407 | * Resolve the fullpath 408 | * 409 | * @param {String} root 410 | * @param {String} entry 411 | * @param {Object} options 412 | * @return {String} 413 | */ 414 | 415 | function fullpath(root, entry, options) { 416 | options = options || {}; 417 | options.catch = options.catch || false; 418 | 419 | var isRelative = './' == entry.slice(0, 2); 420 | var isParent = '..' == entry.slice(0, 2); 421 | var isAbsolute = '/' == entry[0]; 422 | var ret; 423 | 424 | if (isAbsolute) { 425 | ret = join(root, entry); 426 | } else if (isRelative || isParent) { 427 | ret = resolve(root, entry); 428 | } else { 429 | ret = nm(root, entry) || join(root, entry); 430 | } 431 | 432 | if (!exists(ret)) { 433 | var err = new Error(entry + ' does not exist! resolved to: ' + ret); 434 | if (options.catch) return err; 435 | else throw err; 436 | } 437 | 438 | return ret; 439 | } 440 | 441 | /** 442 | * Safely decode 443 | * 444 | * @param {String} path 445 | * @return {String} path 446 | */ 447 | 448 | function decode(path) { 449 | try { 450 | return decodeURIComponent(path); 451 | } catch (e) { 452 | return path; 453 | } 454 | } 455 | 456 | /** 457 | * Passthrough 458 | * 459 | * @param {Object} file 460 | * @return {Object} file 461 | */ 462 | 463 | function passthrough(file) { 464 | return file; 465 | } 466 | 467 | /** 468 | * Document.write 469 | * 470 | * @param {String} msg 471 | * @return {String} 472 | */ 473 | 474 | function write_js_error(msg) { 475 | return [ 476 | 'document.addEventListener("DOMContentLoaded", function() {', 477 | 'document.write("', 478 | [ 479 | '
',
480 |       toHTML(escapeHTML(msg)).replace(/(\r\n|\n|\r)/gm, '
').replace(/color\:\#fff\;/g, '').replace(new RegExp(cwd, 'g'), '.'), 481 | '
' 482 | ].join('').replace(/['"]/gm, '\\$&'), 483 | '");', 484 | '});' 485 | ].join(''); 486 | } 487 | 488 | /** 489 | * Document.write 490 | * 491 | * @param {String} msg 492 | * @return {String} 493 | */ 494 | 495 | function write_css_error(msg) { 496 | msg = 'CSS Error: \n\n' + msg; 497 | return [ 498 | 'html:after {', 499 | ' content: "' + msg.replace(/(\r\n|\n|\r)/gm, ' \\A ').replace(new RegExp(cwd, 'g'), '.') + '";', 500 | ' padding: 50px;', 501 | ' white-space: pre-wrap;', 502 | ' position: fixed;', 503 | ' color: white;', 504 | ' font-size: 14pt;', 505 | ' font-family: monospace;', 506 | ' top: 0;', 507 | ' left: 0;', 508 | ' right: 0;', 509 | ' bottom: 0;', 510 | ' background: #FF4743;', 511 | ' border: 2px solid red;', 512 | ' text-shadow: 1px 1px 0 red;', 513 | '}' 514 | ].join('\n'); 515 | } 516 | 517 | /** 518 | * Check if `url` is an HTTP URL. 519 | * 520 | * @param {String} path 521 | * @param {Boolean} 522 | * @api private 523 | */ 524 | 525 | function http(url) { 526 | return url.slice(0, 4) === 'http' 527 | || url.slice(0, 3) === '://' 528 | || false; 529 | } 530 | 531 | /** 532 | * Strip a querystring or hash fragment from a `path`. 533 | * 534 | * @param {String} path 535 | * @return {String} 536 | * @api private 537 | */ 538 | 539 | function stripPath(path) { 540 | return path 541 | .split('?')[0] 542 | .split('#')[0]; 543 | } 544 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-bundle", 3 | "version": "2.0.0", 4 | "description": "Generic asset pipeline with caching, etags, and sourcemaps", 5 | "keywords": [], 6 | "author": "Matthew Mueller ", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/koajs/bundle.git" 10 | }, 11 | "dependencies": { 12 | "ansi-html": "0.0.4", 13 | "browser-resolve": "^1.9.0", 14 | "compressible": "^2.0.2", 15 | "convert-source-map": "^1.1.1", 16 | "csso": "^1.3.11", 17 | "debug": "^2.1.2", 18 | "escape-html": "^1.0.2", 19 | "file-deps": "0.0.8", 20 | "mime-types": "^2.0.9", 21 | "object-assign": "^2.0.0", 22 | "uglify-js": "^2.4.23", 23 | "wrap-fn": "^0.1.4" 24 | }, 25 | "devDependencies": { 26 | "babelify": "^5.0.4", 27 | "browserify": "^10.2.3", 28 | "envify": "^3.4.0", 29 | "myth": "^1.4.0", 30 | "react": "^0.12.2", 31 | "rework": "^1.0.1", 32 | "rework-npm": "^1.0.0", 33 | "roo": "^0.3.2" 34 | }, 35 | "browser": { 36 | "react": "react/dist/react.js" 37 | }, 38 | "main": "index" 39 | } -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | // var Browserify = require('browserify'); 2 | // var supertest = require('supertest'); 3 | // var Bundle = require('./'); 4 | // var roo = require('roo')(); 5 | 6 | // var bundle = Bundle({}, function(file, fn) { 7 | // Browserify({ debug: file.debug }) 8 | // .add(file.path) 9 | // .transform(require('babelify')) 10 | // .bundle(fn); 11 | // }); 12 | 13 | // roo.use(bundle()); 14 | 15 | // bundle('new.js'); 16 | // bundle('react'); 17 | 18 | // var app = roo.listen(); 19 | // supertest(app) 20 | // .get('/new.js') 21 | // .end(function(err, res) { 22 | // if (err) throw err; 23 | // console.log('content', res.text); 24 | 25 | // supertest(app) 26 | // .get('/new.map.json') 27 | // .end(function(err, res) { 28 | // if (err) throw err; 29 | // console.log(res.text); 30 | // supertest(app) 31 | // .get('/react') 32 | // .end(function(err, res) { 33 | // if (err) throw err; 34 | // console.log(res.text); 35 | // }) 36 | // }) 37 | 38 | 39 | // }); 40 | -------------------------------------------------------------------------------- /test/bundle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var request = require('supertest'); 6 | var join = require('path').join; 7 | var assert = require('assert'); 8 | var Bundle = require('..'); 9 | var http = require('http'); 10 | var roo = require('roo'); 11 | 12 | var fixtures = join(__dirname, 'fixtures'); 13 | 14 | describe('bundle', function() { 15 | 16 | it('should support mounts', function(done) { 17 | var bundle = Bundle({ root: fixtures }, function(file) { 18 | file.src = "some js asset"; 19 | return file; 20 | }) 21 | 22 | var app1 = roo(fixtures); 23 | app1.use(bundle('/simple.js')); 24 | 25 | var app2 = roo(join(fixtures, 'mount')); 26 | app2.use(bundle('/mount/mount.js')); 27 | 28 | app1.mount('/app2', app2); 29 | 30 | request(app1.listen()) 31 | .get('/simple.js') 32 | .end(function(err, res) { 33 | if (err) return done(err); 34 | assert.equal('some js asset', res.text); 35 | 36 | request(app1.listen()) 37 | .get('/mount/mount.js') 38 | .end(function(err, res) { 39 | if (err) return done(err); 40 | assert.equal('some js asset', res.text); 41 | done(); 42 | }) 43 | }) 44 | 45 | }) 46 | 47 | }) 48 | -------------------------------------------------------------------------------- /test/fixtures/mount/mount.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koajs/bundle/e93b13fb3693883c632cc1dd7641366635d62ab2/test/fixtures/mount/mount.js -------------------------------------------------------------------------------- /test/fixtures/simple.js: -------------------------------------------------------------------------------- 1 | console.log('simple'); 2 | --------------------------------------------------------------------------------