package/LICENSE.md0000644000175000017500000000271411633452060013715 0ustar ianwardianwardCopyright (c), Development Seed All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name "Development Seed" nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package/README.md0000644000175000017500000000344511633452060013572 0ustar ianwardianward# Mirror Aggregates JavaScript, CSS and any other text files for serving them to browsers with [express](http://expressjs.com/). Supports wrapping and postprocessing outputs. A *mirror* can contain files, plain source code or other mirrors. ### Usage ```javascript var mirror = require('mirror'); // Mirror guesses the MIME type based on the first file's extension. var styles = new mirror([ __dirname + '/assets/main.css', __dirname + '/assets/layout.css' ]); // Proving direct source input requires specifying the MIME type manually. var configuration = new mirror([ // Mirror automatically inserts line breaks and semicolons before/after // each item in a "js" type mirror. mirror('var basepath = "/"'), mirror('var config = ' + JSON.stringify(config)), // You can add functions to the mirror. They will be called on each request. mirror(function(callback, req, res) { callback(null, 'var url = ' + JSON.stringify(req.url)); }) ], { type: 'js', maxAge: 60 // Only cache configuration file for 60 seconds. }); // Store the array of files and remove or add files on-the-fly. var files = [ require.resolve('underscore'), require.resolve('backbone'), require.resolve('mymodule/client.js'), // Add other mirrors configuration ]; // Add the mirrors to your express server. app.get('/assets/style.css', styles); app.get('/assets/configuration.json', configuration); app.get('/assets/scripts.js', new mirror(files, { minify: true })); ``` **NOTE:** Mirror loads the requested files from disk for every request. It is meant to run behind a reverse proxy that caches. You can control the cache time with `maxAge` (in seconds) in the options hash. ### Authors - [Young Hahn](https://github.com/yhahn) - [Konstantin Käfer](https://github.com/kkaefer) package/mirror.js0000644000175000017500000001153211633452060014157 0ustar ianwardianwardvar env = process.env.NODE_ENV || 'development'; var fs = require('fs'); var path = require('path'); var uglify = require('uglify-js'); module.exports = exports = Mirror; function Mirror(assets, options) { if (!Array.isArray(assets)) { if (this instanceof Mirror) { throw new Error('First parameter must be an array.'); } else return { content: assets, filename: options || null }; } // Make `instanceof Mirror` work and allow recursive embedding. assets.__proto__ = this; if (!options) options = {}; // Content-Type if (!options.type) { if (assets.length && !assets[0].join) { options.type = path.extname(assets[0]).toLowerCase(); } else { options.type = '.txt'; } } else if (options.type[0] !== '.') { options.type = '.' + options.type.toLowerCase(); } // Cache-Control if (!('maxAge' in options)) options.maxAge = Mirror.defaults.maxAge; // 2 weeks // Separator if (!('separator' in options)) options.separator = Mirror.defaults.separator; // Minify if (!('minify' in options)) options.minify = Mirror.defaults.minify; // Setup header fields if (!options.headers) options.headers = {}; if (!('Content-Type' in options.headers)) { options.headers['Content-Type'] = Mirror.headers[options.type] || 'text/plain'; } if (!('Cache-Control' in options.headers)) { options.headers['Cache-Control'] = 'max-age=' + options.maxAge; } this.assets = assets; this.options = options; this.handler = this.handler.bind(this); this.content = this.content.bind(this); return assets; }; // Make `instanceof Array` work on return values. Mirror.prototype.__proto__ = Array.prototype; Mirror.defaults = { maxAge: env === 'production' ? 1209600 : 0, separator: '\n', minify: env === 'production' }; Mirror.headers = { '.js': 'application/javascript', '.json': 'application/json', '.css': 'text/css' }; Mirror.wrappers = { '.js': function(content, filename, options) { return '\n;' + content + '\n;'; } }; Mirror.processors = { '.js': function(content, options) { if (options.minify) { var ast = uglify.parser.parse(content); ast = uglify.uglify.ast_mangle(ast); ast = uglify.uglify.ast_squeeze(ast); return uglify.uglify.gen_code(ast); } else { return content; } } }; // Allow adding the files "array" as a express callback directly. This is a // shortcut that only works with express because it calls files.call(). // In new code, use files.handler instead. Mirror.prototype.call = function(_, req, res, next) { return this.handler(req, res, next); }; Mirror.prototype.handler = function(req, res, next) { this.content(function(err, data) { if (err) return next(err); if (Mirror.processors[this.options.type]) { data = Mirror.processors[this.options.type](data, this.options); } res.send(data, this.options.headers); }.bind(this), req, res); }; function filename(obj) { return typeof obj === 'string' ? obj : obj.filename || null; } Mirror.prototype.content = function(callback, req, res) { if (this.options.sort) this.options.sort(this.assets); if (!this.assets.length) return callback(null, ''); var result = []; var pending = this.assets.length; var cancelled = false; var done = function() { cancelled = true; if (this.options.wrapper) for (var i = 0; i < result.length; i++) { result[i] = this.options.wrapper(result[i], filename(this.assets[i]), this.options); } if (Mirror.wrappers[this.options.type]) for (var i = 0; i < result.length; i++) { result[i] = Mirror.wrappers[this.options.type](result[i], filename(this.assets[i]), this.options); } callback(null, result.join(this.options.separator)); }.bind(this); this.assets.forEach(function(asset, i) { if (typeof asset.content === 'function') { asset.content(loaded, req, res); } else if (typeof asset.content !== 'undefined') { loaded(null, '' + asset.content); } else { fs.readFile(asset, 'utf8', loaded); } function loaded(err, data) { if (cancelled) { return; } if (err) { cancelled = true; return callback(err); } result[i] = data; if (!--pending) done(); } }); }; // Compatibility to 0.2 Mirror.assets = function(assets, options) { if (!Array.isArray(assets)) assets = [ assets ]; return new Mirror(assets, options); }; Mirror.source = function(sources, options) { if (!Array.isArray(sources)) sources = [ sources ]; return new Mirror(sources, options); }; package/package.json0000644000175000017500000000047411633452060014600 0ustar ianwardianward{ "name": "mirror", "version": "0.3.3", "author": "Development Seed (http://www.developmentseed.org)", "main": "./mirror", "dependencies": { "uglify-js": "1.0.2" }, "devDependencies": { "express": "2.3.x", "expresso" : "0.7.x" } } package/test/compat-0.2.test.js0000644000175000017500000000231111633452060016355 0ustar ianwardianwardvar assert = require('assert'); var express = require('express'); var mirror = require('..'); function contentType(type) { return function(res) { assert.equal(res.headers['content-type'], type); }; } var server = express.createServer(); var assets3 = mirror.assets([ __dirname + '/fixtures/foo.js', __dirname + '/fixtures/bar.js', __dirname + '/fixtures/baz.js', ]); server.get('/assets/3', assets3); exports['test file serving 3'] = function() { assert.response(server, { url: '/assets/3' }, { body: '\n;alert("Hello World");\n;\n\n;alert("Hello bar");\n;\n\n;alert("Hello baz");\n;', status: 200 }, contentType('application/javascript; charset=utf-8')) }; var assets13 = mirror.source([ { filename: 'red.css', content: '#foo { color: red; }' }, { filename: 'blue.css', content: '#bar { color: blue; }' } ], { type: '.css' }); server.get('/assets/13', assets13); exports['test file serving 13'] = function() { assert.response(server, { url: '/assets/13' }, { body: '#foo { color: red; }\n#bar { color: blue; }', status: 200 }, contentType('text/css; charset=utf-8')) };package/test/mirror.test.js0000644000175000017500000001547411633452060016125 0ustar ianwardianwardvar assert = require('assert'); var express = require('express'); var mirror = require('..'); function contentType(type) { return function(res) { assert.equal(res.headers['content-type'], type); }; } var server = express.createServer(); exports['test creation failure'] = function() { assert.throws(function() { new mirror(); }, /First parameter must be an array/); }; var assets1 = new mirror([]); server.get('/assets/1', assets1); exports['test file serving 1'] = function() { assert.response(server, { url: '/assets/1' }, { body: '', status: 200 }, contentType('text/plain; charset=utf-8')) }; var assets2 = new mirror([ __dirname + '/fixtures/foo.js' ]); server.get('/assets/2', assets2); exports['test file serving 2'] = function() { assert.response(server, { url: '/assets/2' }, { body: '\n;alert("Hello World");\n;', status: 200 }, contentType('application/javascript; charset=utf-8')) }; var assets3 = new mirror([ __dirname + '/fixtures/foo.js', __dirname + '/fixtures/bar.js', __dirname + '/fixtures/baz.js', ]); server.get('/assets/3', assets3); exports['test file serving 3'] = function() { assert.response(server, { url: '/assets/3' }, { body: '\n;alert("Hello World");\n;\n\n;alert("Hello bar");\n;\n\n;alert("Hello baz");\n;', status: 200 }, contentType('application/javascript; charset=utf-8')) }; var assets4 = new mirror([ __dirname + '/fixtures/foo.js', mirror('foo();') ]); server.get('/assets/4', assets4); exports['test file serving 4'] = function() { assert.response(server, { url: '/assets/4' }, { body: '\n;alert("Hello World");\n;\n\n;foo();\n;', status: 200 }, contentType('application/javascript; charset=utf-8')) }; var assets5 = new mirror([ __dirname + '/fixtures/foo.css', mirror('#foo { color: red; }') ], { type: '.css' }); server.get('/assets/5', assets5); exports['test file serving 5'] = function() { assert.response(server, { url: '/assets/5' }, { body: 'body { font-family: Helvetica; }\n#foo { color: red; }', status: 200 }, contentType('text/css; charset=utf-8')) }; // Test nested mirrors with first item a mirror (should return text/plain) var assets6 = new mirror([ new mirror([ __dirname + '/fixtures/foo.js', __dirname + '/fixtures/bar.js' ]), __dirname + '/fixtures/baz.js' ]); server.get('/assets/6', assets6); exports['test file serving 6'] = function() { assert.response(server, { url: '/assets/6' }, { body: '\n;alert("Hello World");\n;\n\n;alert("Hello bar");\n;\nalert("Hello baz");', status: 200 }, contentType('text/plain; charset=utf-8')) }; // Test nested mirrors. var assets7 = new mirror([ new mirror([ __dirname + '/fixtures/foo.js', __dirname + '/fixtures/bar.js' ]), __dirname + '/fixtures/baz.js' ], { type: '.js' }); server.get('/assets/7', assets7); exports['test file serving 7'] = function() { assert.response(server, { url: '/assets/7' }, { body: '\n;\n;alert("Hello World");\n;\n\n;alert("Hello bar");\n;\n;\n\n;alert("Hello baz");\n;', status: 200 }, contentType('application/javascript; charset=utf-8')) }; // Test nested mirrors with second item a mirror (should return application/javascript) var assets8 = new mirror([ __dirname + '/fixtures/baz.js', new mirror([ __dirname + '/fixtures/foo.js', __dirname + '/fixtures/bar.js' ]), ]); server.get('/assets/8', assets8); exports['test file serving 8'] = function() { assert.response(server, { url: '/assets/8' }, { body: '\n;alert("Hello baz");\n;\n\n;\n;alert("Hello World");\n;\n\n;alert("Hello bar");\n;\n;', status: 200 }, contentType('application/javascript; charset=utf-8')) }; // Test wrapping var assets9 = new mirror([ __dirname + '/fixtures/bar.js' ], { wrapper: function wrapFn(content, filename, options) { assert.equal(content, 'alert(\"Hello bar\");'); assert.equal(filename, __dirname + '/fixtures/bar.js'); assert.equal(options.wrapper, wrapFn); return 'compressed content'; } }); server.get('/assets/9', assets9); exports['test file serving 9'] = function() { assert.response(server, { url: '/assets/9' }, { body: '\n;compressed content\n;', status: 200 }, contentType('application/javascript; charset=utf-8')) }; // Test custom headers. var assets10 = new mirror([ mirror('\x00\x00\x00') ], { headers: { 'Content-Type': 'application/octet-stream' }, separator: '' }); server.get('/assets/10', assets10); exports['test file serving 10'] = function() { assert.response(server, { url: '/assets/10' }, { body: '\0\0\0', status: 200 }, contentType('application/octet-stream; charset=utf-8')) }; // Test nested wrapping var assets11 = new mirror([ __dirname + '/fixtures/bar.js', new mirror([ mirror('content')], { wrapper: function wrapFn(content, filename, options) { assert.equal(content, 'content'); assert.equal(filename, null); assert.equal(options.wrapper, wrapFn); return 'inner compressed content'; } }) ], { wrapper: function wrapFn(content, filename, options) { if (content === 'inner compressed content') return content; assert.equal(content, 'alert(\"Hello bar\");'); assert.equal(filename, __dirname + '/fixtures/bar.js'); assert.equal(options.wrapper, wrapFn); return 'compressed content'; } }); server.get('/assets/11', assets11); exports['test file serving 11'] = function() { assert.response(server, { url: '/assets/11' }, { body: '\n;compressed content\n;\n\n;inner compressed content\n;', status: 200 }, contentType('application/javascript; charset=utf-8')) }; // Test uglifying JS var assets12 = new mirror([ __dirname + '/fixtures/sample.js' ], { minify: true }); server.get('/assets/12', assets12); exports['test file serving 12'] = function() { assert.response(server, { url: '/assets/12' }, { body: 'function baz(a){var b=a;return b}', status: 200 }, contentType('application/javascript; charset=utf-8')) }; var assets13 = new mirror([ mirror('#foo { color: red; }'), mirror(function(callback, req, res) { callback(null, '/*' + req.url + '*/ #bar { color: blue; }'); }) ], { type: '.css' }); server.get('/assets/13', assets13); exports['test file serving 13'] = function() { assert.response(server, { url: '/assets/13' }, { body: '#foo { color: red; }\n/*/assets/13*/ #bar { color: blue; }', status: 200 }, contentType('text/css; charset=utf-8')) }; package/test/fixtures/bar.js0000644000175000017500000000002311633452060016232 0ustar ianwardianwardalert("Hello bar");package/test/fixtures/baz.js0000644000175000017500000000002311633452060016242 0ustar ianwardianwardalert("Hello baz");package/test/fixtures/foo.css0000644000175000017500000000004011633452060016424 0ustar ianwardianwardbody { font-family: Helvetica; }package/test/fixtures/foo.js0000644000175000017500000000002511633452060016253 0ustar ianwardianwardalert("Hello World");package/test/fixtures/sample.js0000644000175000017500000000042411633452060016754 0ustar ianwardianward// (c) Development Seed // Tremendous amounts of whitespace in this file. function baz(bar) // Begin function baz { // Assigning bar to foo var foo = bar; // Returning foo. return foo; // End function baz }