├── .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 | [](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 |
--------------------------------------------------------------------------------