├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── index.js ├── plugins │ ├── coffeescript.js │ ├── css.js │ ├── javascript.js │ ├── less.js │ └── stylus.js └── stub.js ├── package.json └── test ├── files ├── a.css ├── a2.css ├── b.js ├── b2.js ├── c.coffee ├── d.styl ├── d2.styl ├── e.less └── e2.less └── servitude-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.4 4 | - 0.6 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Jerry Sievert 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Servitude 2 | 3 | [![Build Status](https://secure.travis-ci.org/JerrySievert/servitude.png)](http://travis-ci.org/JerrySievert/servitude) 4 | 5 | Super fast sugar for optimizing CSS and JavaScript. 6 | 7 | Servitude combines CSS and JavaScript into a single fast and cacheable file, speeding up your site without a ton of extra work. 8 | 9 | It's easy, just drop your JavaScript and CSS into a directory, point to it, and list out what you want to include in the order you want to include it. `servitude` will inject it into the DOM and your application will be better for it. No more tons of requests, a single request and everything becomes ready to use. 10 | 11 | Optimize without even thinking about it. 12 | 13 | Support out of the box for: 14 | 15 | * CSS 16 | * JavaScript 17 | * CoffeeScript 18 | * Stylus 19 | * Less 20 | * Pluggable Filters 21 | 22 | # Installing 23 | 24 | $ npm install servitude 25 | 26 | # Usage 27 | 28 | ## Server Side 29 | 30 | var servitude = require('servitude'); 31 | var bricks = require('bricks'); 32 | 33 | var appServer = new bricks.appserver(); 34 | 35 | appServer.addRoute("/servitude(.+)", servitude, { basedir: "./files" }); 36 | var server = appServer.createServer(); 37 | 38 | server.listen(3000); 39 | 40 | ## Client Side 41 | 42 | 43 | 44 | 45 | 46 | 47 | # Advanced Usage 48 | 49 | ## Server Side 50 | 51 | ### Caching 52 | 53 | Enabling caching stores requested files in memory, and only re-retrieves and re-processes a file if it has been changed on disk. 54 | 55 | appServer.addRoute("/servitude/(.+)", servitude, { basedir: "./files", cache: true }); 56 | 57 | ### Uglify 58 | 59 | If `uglify` is enabled in the `options`, an attempt is made to `uglify` any JavaScript that has been requested. Note, this occurs even if the JavaScript has been previously minified, as well as for any `.coffee` file that has been compiled. This may not be desired behavior, so this is turned off by default 60 | 61 | appServer.addRoute("/servitude/(.+)", servitude, { basedir: "./files", uglify: true }); 62 | 63 | ### Filters 64 | 65 | Filters are more powerful and allow you to process any file as you would like. This is a good way to add something like `Handlebars` template compilation. Simply set the `data` property on the `record` to the `JavaScript` or `CSS` that should be injected, and the `processed` property on the `record` to a string containing either a `servitude.injectCSS()` or `servitude.injectJS()` call containing `JSON.stringify(record)`: 66 | 67 | var filter = function (record, options, callback) { 68 | record.data = 'var Templates = Templates || { };' + 69 | 'Templates[\"' + record.filename + '\"] = Handlebars.template("' + 70 | handlebars.precompile(record.data) + '");'; 71 | 72 | record.processed = 'injectJS(' + JSON.stringify(record) + ');'; 73 | 74 | callback(null, record); 75 | }; 76 | 77 | appServer.addRoute("/servitude/(.+)", 78 | servitude, 79 | { basedir: "./files", filters: { ".+handlebars$": filter } }); 80 | 81 | 82 | ## Client Side 83 | 84 | A `servitude` object is returned with all methods for injection into the DOM. 85 | 86 | 87 | if (servitude.errors.length) { 88 | console.log("errors: "); 89 | console.dir(servitude.errors); 90 | } 91 | 92 | Injection occurs via the `servitude.injectCSS()` and `servitude.injectJS()` methods upon load. -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var fs = require('fs'); 3 | 4 | var stub = fs.readFileSync(__dirname + '/stub.js', "binary"); 5 | 6 | var cache = { }; 7 | 8 | 9 | function readFile (options, filename, modified, index, callback) { 10 | fs.stat(options.basedir + "/" + filename, function (err, stats) { 11 | if (err) { 12 | callback("Unable to find " + filename); 13 | } else { 14 | if (cache[filename] && cache[filename].modified >= stats.mtime.valueOf()) { 15 | var record = cache[filename]; 16 | record.index = index; 17 | callback(null, record); 18 | } else { 19 | fs.readFile(options.basedir + "/" + filename, "binary", function (err, data) { 20 | if (err) { 21 | callback("Unable to read " + filename); 22 | } else { 23 | var record = { 24 | data: data, 25 | modified: stats.mtime.valueOf(), 26 | filename: filename 27 | }; 28 | 29 | record.index = index; 30 | 31 | if (record.processed === undefined) { 32 | for (var i in options.filters) { 33 | if (record.filename.match(i)) { 34 | options.filters[i](record, options, callback); 35 | break; 36 | } 37 | } 38 | } else { 39 | callback(null, record); 40 | } 41 | } 42 | }); 43 | } 44 | } 45 | }); 46 | } 47 | 48 | function setHeaders (response, maxage) { 49 | response.setHeader('Date', new Date().toUTCString()); 50 | 51 | response.setHeader('Last-Modified', new Date(maxage)); 52 | response.setHeader('Age', parseInt((new Date().valueOf() - maxage) / 1000, 10)); 53 | } 54 | 55 | exports.plugin = function (request, response, options) { 56 | options = options || { }; 57 | 58 | options.separator = options.separator || ','; 59 | options.filters = options.filters || { }; 60 | 61 | options.filters[".+js$"] = options.filters[".+js$"] || require('./plugins/javascript'); 62 | options.filters[".+css$"] = options.filters[".+css$"] || require('./plugins/css'); 63 | options.filters[".+coffee$"] = options.filters[".+coffee$"] || require('./plugins/coffeescript'); 64 | options.filters[".+styl$"] = options.filters[".+styl$"] || require('./plugins/stylus'); 65 | options.filters[".+less$"] = options.filters[".+less$"] || require('./plugins/less'); 66 | 67 | var parts = request.url.match(options.path); 68 | var files = parts[1].split(options.separator); 69 | 70 | var output = [ ], 71 | errors = [ ]; 72 | 73 | var modified; 74 | 75 | var count = files.length, 76 | current = 0, 77 | cached = 0, 78 | maxage = 0; 79 | 80 | function callback (err, record) { 81 | current++; 82 | 83 | if (err) { 84 | errors.push(err); 85 | } else { 86 | output.push(record); 87 | 88 | if (modified >= record.modified) { 89 | cached++; 90 | } 91 | 92 | maxage = Math.max(maxage, record.modified); 93 | 94 | if (record.processed === undefined) { 95 | record.processed = record.data; 96 | } 97 | 98 | if (options.cache) { 99 | cache[record.filename] = record; 100 | } 101 | } 102 | 103 | if (current === count) { 104 | if (cached === count) { 105 | response.statusCode(304); 106 | } else if (count === 1 && !err) { 107 | response.setHeader('Content-Type', record.mimetype); 108 | setHeaders(response, maxage); 109 | 110 | response.write(record.data); 111 | } else { 112 | output = output.sort(function (a, b) { 113 | return a.index - b.index; 114 | }); 115 | 116 | var results = stub; 117 | errors.forEach(function (elem) { 118 | results += 'servitude.errors.push("' + elem + '");' + "\n"; 119 | }); 120 | 121 | results += output.map(function (elem) { return elem.processed; }).join("\n"); 122 | 123 | response.setHeader('Content-Type', 'text/javascript'); 124 | setHeaders(response, maxage); 125 | 126 | response.write(results); 127 | } 128 | 129 | response.end(); 130 | } 131 | } 132 | 133 | if (request.headers['if-modified-since']) { 134 | modified = Date.parse(request.headers['if-modified-since']); 135 | } 136 | 137 | var index = 0; 138 | 139 | files.forEach(function (elem, index) { 140 | readFile(options, elem, modified, ++index, callback); 141 | }); 142 | }; 143 | })(); -------------------------------------------------------------------------------- /lib/plugins/coffeescript.js: -------------------------------------------------------------------------------- 1 | var jsp = require("uglify-js").parser, 2 | pro = require("uglify-js").uglify, 3 | coffee = require("coffee-script"); 4 | 5 | exports = module.exports = function (record, options, callback) { 6 | record.data = coffee.compile(record.data); 7 | 8 | if (options.uglify) { 9 | var ast = jsp.parse(record.data); 10 | ast = pro.ast_mangle(ast); 11 | ast = pro.ast_squeeze(ast); 12 | 13 | record.data = pro.gen_code(ast); 14 | } 15 | 16 | record.mimetype = "text/javascript"; 17 | record.processed = "servitude.injectJS(" + JSON.stringify(record) + ");"; 18 | 19 | callback(null, record); 20 | }; -------------------------------------------------------------------------------- /lib/plugins/css.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = function (record, options, callback) { 2 | record.mimetype = "text/css"; 3 | record.processed = 'servitude.injectCSS(' + JSON.stringify(record) + ');'; 4 | 5 | callback(null, record); 6 | }; -------------------------------------------------------------------------------- /lib/plugins/javascript.js: -------------------------------------------------------------------------------- 1 | var jsp = require("uglify-js").parser, 2 | pro = require("uglify-js").uglify; 3 | 4 | 5 | exports = module.exports = function (record, options, callback) { 6 | if (options.uglify) { 7 | var ast = jsp.parse(record.data); 8 | ast = pro.ast_mangle(ast); 9 | ast = pro.ast_squeeze(ast); 10 | 11 | record.data = pro.gen_code(ast); 12 | } 13 | 14 | record.mimetype = "text/javascript"; 15 | record.processed = "servitude.injectJS(" + JSON.stringify(record) + ");"; 16 | 17 | callback(null, record); 18 | }; -------------------------------------------------------------------------------- /lib/plugins/less.js: -------------------------------------------------------------------------------- 1 | var less = require('less'); 2 | 3 | exports = module.exports = function (record, options, callback) { 4 | options.less = options.less || { }; 5 | 6 | less.render(record.data, options.less, function(err, css) { 7 | if (err) { 8 | callback(err.message); 9 | } else { 10 | record.data = css; 11 | record.mimetype = "text/css"; 12 | record.processed = 'servitude.injectCSS(' + JSON.stringify(record) + ');'; 13 | 14 | callback(null, record); 15 | } 16 | }); 17 | }; -------------------------------------------------------------------------------- /lib/plugins/stylus.js: -------------------------------------------------------------------------------- 1 | var stylus = require('stylus'); 2 | 3 | exports = module.exports = function (record, options, callback) { 4 | stylus(record.data).render(function(err, css) { 5 | if (err) { 6 | callback(err); 7 | } else { 8 | record.data = css; 9 | record.mimetype = "text/css"; 10 | record.processed = 'servitude.injectCSS(' + JSON.stringify(record) + ');'; 11 | 12 | callback(null, record); 13 | } 14 | }); 15 | }; -------------------------------------------------------------------------------- /lib/stub.js: -------------------------------------------------------------------------------- 1 | var servitude = servitude || { 2 | "errors": [ ], 3 | "injectCSS": function (data) { 4 | var styleElem = document.createElement("style"); 5 | 6 | styleElem.setAttribute("data-injected-css", data.filename); 7 | styleElem.setAttribute("type", "text/css"); 8 | styles = document.getElementsByTagName("style"); 9 | domTarget = styles.length ? styles[styles.length - 1] : document.getElementsByTagName("script")[0]; 10 | domTarget.parentNode.appendChild(styleElem); 11 | if (styleElem.styleSheet) { 12 | styleElem.styleSheet.cssText = data.data; 13 | } else { 14 | styleElem.appendChild(document.createTextNode(data.data)); 15 | } 16 | }, 17 | "injectJS": function (data) { 18 | var jsElem = document.createElement("script"); 19 | 20 | jsElem.setAttribute("data-injected-javascript", data.filename); 21 | jsElem.setAttribute("type", "text/javascript"); 22 | domTarget = document.getElementsByTagName("script")[0]; 23 | domTarget.parentNode.appendChild(jsElem); 24 | jsElem.text = data.data; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Jerry Sievert (http://legitimatesounding.com/blog/index.html)", 3 | "name": "servitude", 4 | "description": "JavaScript and CSS Sugar", 5 | "version": "0.3.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "http://github.com/JerrySievert/servitude.git" 9 | }, 10 | "main": "lib/index.js", 11 | "keywords": ["javascript", "css", "bricks", "middleware", "sugar"], 12 | "license" : "MIT/X11", 13 | "engines": { 14 | "node": ">=0.4.3" 15 | }, 16 | "dependencies": { 17 | "uglify-js": "1.2.6", 18 | "coffee-script": "~1.2.0", 19 | "stylus": "~0.24.0", 20 | "less": "1.3.x" 21 | }, 22 | "devDependencies": { 23 | "vows": ">=0.5.0", 24 | "mock-request-response": ">=0.1.0", 25 | "cromag": "~0.1.0" 26 | }, 27 | "scripts": { 28 | "test": "vows --spec" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/files/a.css: -------------------------------------------------------------------------------- 1 | h1 { color: red; font-size: 22px; } -------------------------------------------------------------------------------- /test/files/a2.css: -------------------------------------------------------------------------------- 1 | h2 { color: blue; font-size: 18px; } -------------------------------------------------------------------------------- /test/files/b.js: -------------------------------------------------------------------------------- 1 | console.log("hello from a"); -------------------------------------------------------------------------------- /test/files/b2.js: -------------------------------------------------------------------------------- 1 | console.log("hello from b"); -------------------------------------------------------------------------------- /test/files/c.coffee: -------------------------------------------------------------------------------- 1 | cubes = (math.cube num for num in list) -------------------------------------------------------------------------------- /test/files/d.styl: -------------------------------------------------------------------------------- 1 | body 2 | background-color #abc 3 | 4 | h1 5 | color #cba -------------------------------------------------------------------------------- /test/files/d2.styl: -------------------------------------------------------------------------------- 1 | body 2 | background-color #abc 3 | 4 | h1 5 | color -------------------------------------------------------------------------------- /test/files/e.less: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #abc; 3 | h1 { 4 | color: #cba; 5 | } 6 | } -------------------------------------------------------------------------------- /test/files/e2.less: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #abc; 3 | h1 { 4 | color: #cba; 5 | } 6 | -------------------------------------------------------------------------------- /test/servitude-test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | servitude = require('../lib/index.js'), 4 | mrequest = require('mock-request-response/server-request'), 5 | mresponse = require('mock-request-response/server-response'), 6 | fs = require('fs'), 7 | Cromag = require('cromag'); 8 | 9 | var stub = fs.readFileSync(__dirname + '/../lib/stub.js', "binary"); 10 | 11 | vows.describe('Servitude').addBatch({ 12 | 'when a single css file is requested': { 13 | topic: function () { 14 | var req = new mrequest.request(); 15 | req.url = "/servitude/a.css"; 16 | 17 | var res = new mresponse.response(); 18 | var callback = this.callback; 19 | res.end = function () { callback(undefined, this._internals.buffer); }; 20 | 21 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files" }); 22 | }, 23 | 'only the css is returned': function (err, data) { 24 | var mtime = fs.statSync(__dirname + '/files/a.css').mtime.valueOf(); 25 | 26 | assert.equal(data, 'h1 { color: red; font-size: 22px; }'); 27 | } 28 | }, 29 | 'when multiple css files are requested': { 30 | topic: function () { 31 | var req = new mrequest.request(); 32 | req.url = "/servitude/a.css,/a2.css"; 33 | 34 | var res = new mresponse.response(); 35 | var callback = this.callback; 36 | res.end = function () { callback(undefined, this._internals.buffer); }; 37 | 38 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files" }); 39 | }, 40 | 'a servitude injection for 2 css files is returned': function (err, data) { 41 | var mtimeA = fs.statSync(__dirname + '/files/a.css').mtime.valueOf(); 42 | var mtimeA2 = fs.statSync(__dirname + '/files/a2.css').mtime.valueOf(); 43 | 44 | assert.equal(data, 'var servitude = servitude || {\n "errors": [ ],\n "injectCSS": function (data) {\n var styleElem = document.createElement("style");\n\n styleElem.setAttribute("data-injected-css", data.filename);\n styleElem.setAttribute("type", "text/css");\n styles = document.getElementsByTagName("style");\n domTarget = styles.length ? styles[styles.length - 1] : document.getElementsByTagName("script")[0];\n domTarget.parentNode.appendChild(styleElem);\n if (styleElem.styleSheet) {\n styleElem.styleSheet.cssText = data.data;\n } else {\n styleElem.appendChild(document.createTextNode(data.data));\n }\n },\n "injectJS": function (data) {\n var jsElem = document.createElement("script");\n\n jsElem.setAttribute("data-injected-javascript", data.filename);\n jsElem.setAttribute("type", "text/javascript");\n domTarget = document.getElementsByTagName("script")[0];\n domTarget.parentNode.appendChild(jsElem);\n jsElem.text = data.data;\n }\n};\nservitude.injectCSS({"data":"h1 { color: red; font-size: 22px; }","modified":'+ mtimeA +',"filename":"/a.css","index":1,"mimetype":"text/css"});\nservitude.injectCSS({"data":"h2 { color: blue; font-size: 18px; }","modified":' + mtimeA2 + ',"filename":"/a2.css","index":2,"mimetype":"text/css"});'); 45 | } 46 | }, 47 | 'when a single javascript file is requested': { 48 | topic: function () { 49 | var req = new mrequest.request(); 50 | req.url = "/servitude/b.js"; 51 | 52 | var res = new mresponse.response(); 53 | var callback = this.callback; 54 | res.end = function () { callback(undefined, this._internals.buffer); }; 55 | 56 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files" }); 57 | }, 58 | 'only the javascript is returned': function (err, data) { 59 | var mtime = fs.statSync(__dirname + '/files/b.js').mtime.valueOf(); 60 | 61 | assert.equal(data, 'console.log("hello from a");'); 62 | } 63 | }, 64 | 'when a multiple javascript files are requested': { 65 | topic: function () { 66 | var req = new mrequest.request(); 67 | req.url = "/servitude/b.js,/b2.js"; 68 | 69 | var res = new mresponse.response(); 70 | var callback = this.callback; 71 | res.end = function () { callback(undefined, this._internals.buffer); }; 72 | 73 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files" }); 74 | }, 75 | 'two servitude injections are returned': function (err, data) { 76 | var mtimeB = fs.statSync(__dirname + '/files/b.js').mtime.valueOf(); 77 | var mtimeB2 = fs.statSync(__dirname + '/files/b2.js').mtime.valueOf(); 78 | 79 | assert.equal(data, 'var servitude = servitude || {\n "errors": [ ],\n "injectCSS": function (data) {\n var styleElem = document.createElement("style");\n\n styleElem.setAttribute("data-injected-css", data.filename);\n styleElem.setAttribute("type", "text/css");\n styles = document.getElementsByTagName("style");\n domTarget = styles.length ? styles[styles.length - 1] : document.getElementsByTagName("script")[0];\n domTarget.parentNode.appendChild(styleElem);\n if (styleElem.styleSheet) {\n styleElem.styleSheet.cssText = data.data;\n } else {\n styleElem.appendChild(document.createTextNode(data.data));\n }\n },\n "injectJS": function (data) {\n var jsElem = document.createElement("script");\n\n jsElem.setAttribute("data-injected-javascript", data.filename);\n jsElem.setAttribute("type", "text/javascript");\n domTarget = document.getElementsByTagName("script")[0];\n domTarget.parentNode.appendChild(jsElem);\n jsElem.text = data.data;\n }\n};\nservitude.injectJS({"data":"console.log(\\"hello from a\\");","modified":' + mtimeB + ',"filename":"/b.js","index":1,"mimetype":"text/javascript"});\nservitude.injectJS({"data":"console.log(\\"hello from b\\");","modified":' + mtimeB2 + ',"filename":"/b2.js","index":2,"mimetype":"text/javascript"});'); 80 | } 81 | }, 82 | 'when a single coffeescript file is requested': { 83 | topic: function () { 84 | var req = new mrequest.request(); 85 | req.url = "/servitude/c.coffee"; 86 | 87 | var res = new mresponse.response(); 88 | var callback = this.callback; 89 | res.end = function () { callback(undefined, this._internals.buffer); }; 90 | 91 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files" }); 92 | }, 93 | 'the compiled version is returned': function (err, data) { 94 | var mtime = fs.statSync(__dirname + '/files/c.coffee').mtime.valueOf(); 95 | 96 | assert.equal(data, "(function() {\n var cubes, num;\n\n cubes = (function() {\n var _i, _len, _results;\n _results = [];\n for (_i = 0, _len = list.length; _i < _len; _i++) {\n num = list[_i];\n _results.push(math.cube(num));\n }\n return _results;\n })();\n\n}).call(this);\n"); 97 | } 98 | }, 99 | 'when a single coffeescript file is requested and uglify is turned on': { 100 | topic: function () { 101 | var req = new mrequest.request(); 102 | req.url = "/servitude/c.coffee"; 103 | 104 | var res = new mresponse.response(); 105 | var callback = this.callback; 106 | res.end = function () { callback(undefined, this._internals.buffer); }; 107 | 108 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files", uglify: true }); 109 | }, 110 | 'the compiled and uglified version is returned': function (err, data) { 111 | var mtime = fs.statSync(__dirname + '/files/c.coffee').mtime.valueOf(); 112 | 113 | assert.equal(data, '(function(){var a,b;a=function(){var a,c,d;d=[];for(a=0,c=list.length;a 5| color\n\nexpected \"indent\", got \"eos\"\n\");\n"); 146 | } 147 | }, 148 | 'when a single less file is requested': { 149 | topic: function () { 150 | var req = new mrequest.request(); 151 | req.url = "/servitude/e.less"; 152 | 153 | var res = new mresponse.response(); 154 | var callback = this.callback; 155 | res.end = function () { callback(undefined, this._internals.buffer); }; 156 | 157 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files" }); 158 | }, 159 | 'the compiled version is returned': function (err, data) { 160 | var mtime = fs.statSync(__dirname + '/files/e.less').mtime.valueOf(); 161 | 162 | assert.equal(data, "body {\n background-color: #abc;\n}\nbody h1 {\n color: #cba;\n}\n"); 163 | } 164 | }, 165 | 'when a bad less file is requested': { 166 | topic: function () { 167 | var req = new mrequest.request(); 168 | req.url = "/servitude/e2.less"; 169 | 170 | var res = new mresponse.response(); 171 | var callback = this.callback; 172 | res.end = function () { callback(undefined, this._internals.buffer); }; 173 | 174 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files" }); 175 | }, 176 | 'a servitude object with an error is returned': function (err, data) { 177 | assert.equal(data, "var servitude = servitude || {\n \"errors\": [ ],\n \"injectCSS\": function (data) {\n var styleElem = document.createElement(\"style\");\n\n styleElem.setAttribute(\"data-injected-css\", data.filename);\n styleElem.setAttribute(\"type\", \"text/css\");\n styles = document.getElementsByTagName(\"style\");\n domTarget = styles.length ? styles[styles.length - 1] : document.getElementsByTagName(\"script\")[0];\n domTarget.parentNode.appendChild(styleElem);\n if (styleElem.styleSheet) {\n styleElem.styleSheet.cssText = data.data;\n } else {\n styleElem.appendChild(document.createTextNode(data.data));\n }\n },\n \"injectJS\": function (data) {\n var jsElem = document.createElement(\"script\");\n\n jsElem.setAttribute(\"data-injected-javascript\", data.filename);\n jsElem.setAttribute(\"type\", \"text/javascript\");\n domTarget = document.getElementsByTagName(\"script\")[0];\n domTarget.parentNode.appendChild(jsElem);\n jsElem.text = data.data;\n }\n};\nservitude.errors.push(\"missing closing `}`\");\n"); 178 | } 179 | }, 180 | 'when a multiple less and css files are requested': { 181 | topic: function () { 182 | var req = new mrequest.request(); 183 | req.url = "/servitude/e.less,/a.css"; 184 | 185 | var res = new mresponse.response(); 186 | var callback = this.callback; 187 | res.end = function () { callback(undefined, this._internals.buffer); }; 188 | 189 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files" }); 190 | }, 191 | 'the compiled version is returned for servitude injection': function (err, data) { 192 | var mtimeE = fs.statSync(__dirname + '/files/e.less').mtime.valueOf(); 193 | var mtimeA = fs.statSync(__dirname + '/files/a.css').mtime.valueOf(); 194 | 195 | assert.equal(data, 'var servitude = servitude || {\n "errors": [ ],\n "injectCSS": function (data) {\n var styleElem = document.createElement("style");\n\n styleElem.setAttribute("data-injected-css", data.filename);\n styleElem.setAttribute("type", "text/css");\n styles = document.getElementsByTagName("style");\n domTarget = styles.length ? styles[styles.length - 1] : document.getElementsByTagName("script")[0];\n domTarget.parentNode.appendChild(styleElem);\n if (styleElem.styleSheet) {\n styleElem.styleSheet.cssText = data.data;\n } else {\n styleElem.appendChild(document.createTextNode(data.data));\n }\n },\n "injectJS": function (data) {\n var jsElem = document.createElement("script");\n\n jsElem.setAttribute("data-injected-javascript", data.filename);\n jsElem.setAttribute("type", "text/javascript");\n domTarget = document.getElementsByTagName("script")[0];\n domTarget.parentNode.appendChild(jsElem);\n jsElem.text = data.data;\n }\n};\nservitude.injectCSS({"data":"body {\\n background-color: #abc;\\n}\\nbody h1 {\\n color: #cba;\\n}\\n","modified":' + mtimeE + ',"filename":"/e.less","index":1,"mimetype":"text/css"});\nservitude.injectCSS({"data":"h1 { color: red; font-size: 22px; }","modified":' + mtimeA + ',"filename":"/a.css","index":2,"mimetype":"text/css"});'); 196 | } 197 | }, 198 | 'when an unknown javascript file is requested': { 199 | topic: function () { 200 | var req = new mrequest.request(); 201 | req.url = "/servitude/q.js"; 202 | 203 | var res = new mresponse.response(); 204 | var callback = this.callback; 205 | res.end = function () { callback(undefined, this._internals.buffer); }; 206 | 207 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files" }); 208 | }, 209 | 'a servitude object with an error injection is returned': function (err, data) { 210 | assert.equal(data, 'var servitude = servitude || {\n "errors": [ ],\n "injectCSS": function (data) {\n var styleElem = document.createElement("style");\n\n styleElem.setAttribute("data-injected-css", data.filename);\n styleElem.setAttribute("type", "text/css");\n styles = document.getElementsByTagName("style");\n domTarget = styles.length ? styles[styles.length - 1] : document.getElementsByTagName("script")[0];\n domTarget.parentNode.appendChild(styleElem);\n if (styleElem.styleSheet) {\n styleElem.styleSheet.cssText = data.data;\n } else {\n styleElem.appendChild(document.createTextNode(data.data));\n }\n },\n "injectJS": function (data) {\n var jsElem = document.createElement("script");\n\n jsElem.setAttribute("data-injected-javascript", data.filename);\n jsElem.setAttribute("type", "text/javascript");\n domTarget = document.getElementsByTagName("script")[0];\n domTarget.parentNode.appendChild(jsElem);\n jsElem.text = data.data;\n }\n};\nservitude.errors.push("Unable to find /q.js");\n'); 211 | } 212 | }, 213 | 'when a filter is applied': { 214 | topic: function () { 215 | var req = new mrequest.request(); 216 | req.url = "/servitude/b.js"; 217 | 218 | var res = new mresponse.response(); 219 | var callback = this.callback; 220 | res.end = function () { callback(undefined, this._internals.buffer); }; 221 | 222 | var filter = function (data, options, callback) { 223 | data.data = data.data.replace('hello', 'goodbye'); 224 | 225 | data.processed = 'servitude.injectJS(' + JSON.stringify(data) + ');'; 226 | callback(null, data); 227 | }; 228 | 229 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files", filters: { ".+js$": filter } }); 230 | }, 231 | 'the filtered result is returned': function (err, data) { 232 | var mtime = fs.statSync(__dirname + '/files/b.js').mtime.valueOf(); 233 | 234 | assert.equal(data, "console.log(\"goodbye from a\");"); 235 | } 236 | }, 237 | 'when uglify is specified': { 238 | topic: function () { 239 | var req = new mrequest.request(); 240 | req.url = "/servitude/b.js"; 241 | 242 | var res = new mresponse.response(); 243 | var callback = this.callback; 244 | res.end = function () { callback(undefined, this._internals.buffer); }; 245 | 246 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files", uglify: true }); 247 | }, 248 | 'the uglified version of the code is returned': function (err, data) { 249 | var mtime = fs.statSync(__dirname + '/files/b.js').mtime.valueOf(); 250 | 251 | assert.equal(data, "console.log(\"hello from a\")"); 252 | } 253 | }, 254 | 'when caching is enabled': { 255 | topic: function () { 256 | var req = new mrequest.request(); 257 | req.url = "/servitude/b.js"; 258 | 259 | var res = new mresponse.response(); 260 | var callback = this.callback; 261 | res.end = function () { callback(undefined, this._internals.buffer); }; 262 | 263 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files", uglify: true, cache: true }); 264 | }, 265 | 'data is returned the first time': function (err, data) { 266 | var mtime = fs.statSync(__dirname + '/files/b.js').mtime.valueOf(); 267 | 268 | assert.equal(data, 'console.log("hello from a")'); 269 | }, 270 | 'and returned the second time when if-modified-since is set': { 271 | topic: function () { 272 | var req = new mrequest.request(); 273 | req.url = "/servitude/b.js"; 274 | req.headers['if-modified-since'] = new Cromag().add({days: 1}).toFormat('MMM DD, YYYY'); 275 | 276 | var res = new mresponse.response(); 277 | var callback = this.callback; 278 | var count = 0; 279 | res.end = function () { count++; if (count === 2) { callback(undefined, this._internals.statusCode); } }; 280 | res.statusCode = function (code) { this._internals.statusCode = code; }; 281 | 282 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files", uglify: true, cache: true }); 283 | servitude.plugin(req, res, { path: "/servitude(.+)", basedir: __dirname + "/files", uglify: true, cache: true }); 284 | }, 285 | 'is a 304 statusCode': function (err, statusCode) { 286 | assert.equal(statusCode, 304); 287 | } 288 | } 289 | } 290 | }).export(module); 291 | --------------------------------------------------------------------------------