├── .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 |
--------------------------------------------------------------------------------