Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add connect-assetmanager to Github.

  • Loading branch information...
commit 15e37997c2156add185d23c9a73c2f865143ec3a 0 parents
@mape authored
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2009 Mathias Pettersson, mape@mape.me
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
208 README.md
@@ -0,0 +1,208 @@
+# connect-assetmanager
+
+Middleware for Connect (node.js) for handling your static assets.
+
+<img src="http://mape.me/assetmanager.png" alt="">
+
+## What does it allow you to do?
+* Merge and minify CSS/javascript files
+* Auto regenerates the cache on file change so no need for restart of server or manual action.
+* Run pre/post manipulation on the files
+ * __Use regex to match user agent so you can serve different modified versions of your packed assets based on the requesting browser.__
+* Supplies a reference to the modified dates for all groups through assetManager(groups).cacheTimestamps which can be used for cache invalidation in templates.
+
+### Nifty things you can do with the pre/post manipulation
+* __Replace all url(references to images) with inline base64 data which remove all would be image HTTP requests.__
+* Strip all IE specific code for all other browsers.
+* Fix all the vendor prefixes (-ms -moz -webkit -o) for things like border-radius instead of having to type all each and every time.
+
+
+## Speed test (it does just fine)
+### Running with
+ > connect app -n 4
+
+### Common data
+ Concurrency Level: 240
+ Complete requests: 10000
+ Failed requests: 0
+ Write errors: 0
+
+### Small (reset.css)
+ Document Path: /static/test/small
+ Document Length: 170 bytes
+
+ Time taken for tests: 0.588 seconds
+ Total transferred: 4380001 bytes
+ HTML transferred: 1700000 bytes
+ Requests per second: 17005.50 [#/sec] (mean)
+ Time per request: 14.113 [ms] (mean)
+ Time per request: 0.059 [ms] (mean, across all concurrent requests)
+ Transfer rate: 7273.84 [Kbytes/sec] received
+
+### Larger (jQuery.js)
+ Document Path: /static/test/large
+ Document Length: 100732 bytes
+
+ Time taken for tests: 10.817 seconds
+ Total transferred: 1012772490 bytes
+ HTML transferred: 1009913368 bytes
+ Requests per second: 924.51 [#/sec] (mean)
+ Time per request: 259.597 [ms] (mean)
+ Time per request: 1.082 [ms] (mean, across all concurrent requests)
+ Transfer rate: 91437.43 [Kbytes/sec] received
+
+## Options
+### path (string) - required
+The path to the folder containing the files.
+
+ path: __dirname + '/'
+
+### files (array) - required
+An array of strings containing the filenames of all files in the group.
+
+ files: ['lib.js', 'page.js']
+
+### route (regex as string) - required
+The route that will be matched by Connect.
+
+ route: '/\/assets\/css\/.*\.css'
+
+### dataType (string), ['javascript', 'css']
+The type of data you are trying to optimize, 'javascript' and 'css' is built into the core of the assetManager and will minify them using the appropriate code.
+
+ dataType: 'css'
+
+### preManipulate (object containing functions)
+There are hooks in the assetManager that allow you to programmaticly alter the source of the files you are grouping.
+This can be handy for being able to use custom CSS types in the assetManager or fixing stuff like vendor prefixes in a general fashion.
+
+ 'preManipulate': {
+ // Regexp to match user-agents including MSIE.
+ 'MSIE': [
+ generalManipulation
+ , msieSpecificManipulation
+ ],
+ // Matches all (regex start line)
+ '^': [
+ generalManipulation
+ , fixVendorPrefixes
+ , fixGradients
+ , replaceImageRefToBase64
+ ]
+ }
+
+### postManipulate (object containing functions)
+Same as preManipulate but runs after the files are merged and minified.
+
+### stale (boolean)
+Incase you want to use the asset manager with optimal performance you can set stale to true.
+
+This means that there are no checks for file changes and the cache will therefore not be regenerated. Recommended for deployed code.
+
+### debug (boolean)
+When debug is set to true the files will not be minified, but they will be grouped into one file and modified.
+
+## Example usage
+ var sys = require('sys');
+ var fs = require('fs');
+ var Connect = require('connect');
+ var assetManager = require('./connect-assetmanager/lib/assetmanager');
+ var base64_encode = require('node-base64/base64').encode;
+
+ var root = __dirname + '/public';
+
+ // Fix the vendor prefixes
+ var fixVendorPrefixes = function (fileContent, path, index, lastFile, callback) {
+ // -vendor-border-radius: 5px;
+ callback(fileContent.replace(/-vendor-([^:]+): *([^;]+)/g, '$1: $2; -moz-$1: $2; -webkit-$1: $2; -o-$1: $2; -ms-$1: $2;'));
+ };
+
+ // Dumb fix for simple top down gradients.
+ var fixGradients = function (fileContent, path, index, lastFile, callback) {
+ // gradient: rgba(0,0,0,0.5)_#000;
+ callback(fileContent.replace(/gradient: *([^_]+)_([^;]+)/g, 'background: -webkit-gradient(linear, 0% 0%, 0% 100%, from($1), to($2));background: -moz-linear-gradient(top, $1, $2);'));
+ };
+
+ // Replace all custom data-url with standard url since MSIE can't handle base64.
+ var dummyReplaceImageRefToBase64 = function (fileContent, path, index, lastFile, callback) {
+ // background-image: data-url(/img/button.png);
+ callback(fileContent.replace(/data-url/ig,'url'));
+ };
+
+ // Replace all image references with base64 to reduce base64
+ var replaceImageRefToBase64 = function (fileContent, path, index, lastFile, callback) {
+ // background-image: data-url(/img/button.png);
+ var files = fileContent.match(/data-url\(([^)]+)\)/g);
+ if (!files) {
+ callback(fileContent);
+ return;
+ }
+ fileContent = fileContent.replace(/data-url/g,'url');
+ var callIndex = -1;
+
+ var handleFiles = function(content, recursion) {
+ if (callIndex < files.length-1) {
+ callIndex++;
+ var filePath = files[callIndex].replace(/(data-url\(|\))/g,'');
+ fs.readFile(root+filePath, function (err, data) {
+ if (err) {
+ throw err;
+ }
+ content = content.replace(new RegExp(filePath), 'data:image/png;base64,'+base64_encode(data));
+ handleFiles(content, path, index, lastFile, handleFiles);
+ });
+ } else {
+ callback(content);
+ }
+ };
+ handleFiles(fileContent, handleFiles);
+ };
+
+ var Server = module.exports = Connect.createServer();
+
+ Server.use('/',
+ Connect.responseTime()
+ , Connect.logger()
+ );
+
+ var assetManagerGroups = {
+ 'js': {
+ 'route': /\/static\/js\/[0-9]+\/.*\.js/
+ , 'path': './public/js/'
+ , 'dataType': 'javascript'
+ , 'files': [
+ 'jquery.js'
+ , 'jquery.client.js'
+ ]
+ }, 'css': {
+ 'route': /\/static\/css\/[0-9]+\/.*\.css/
+ , 'path': './public/css/'
+ , 'dataType': 'css'
+ , 'files': [
+ 'reset.css'
+ , 'style.css'
+ ]
+ , 'preManipulate': {
+ // Regexp to match user-agents including MSIE.
+ 'MSIE': [
+ fixVendorPrefixes
+ , dummyReplaceImageRefToBase64
+ ],
+ // Matches all (regex start line)
+ '^': [
+ fixVendorPrefixes
+ , fixGradients
+ , replaceImageRefToBase64
+ ]
+ }
+ }
+ };
+
+ var assetsManagerMiddleware = assetManager(assetManagerGroups);
+ Server.use('/'
+ , Connect.conditionalGet()
+ , Connect.cache()
+ , Connect.gzip()
+ , assetsManagerMiddleware
+ , Connect.staticProvider(root)
+ );
182 deps/cssmin.js
@@ -0,0 +1,182 @@
+/**
+ * cssmin.js
+ * Author: Stoyan Stefanov - http://phpied.com/
+ * This is a JavaScript port of the CSS minification tool
+ * distributed with YUICompressor, itself a port
+ * of the cssmin utility by Isaac Schlueter - http://foohack.com/
+ * Permission is hereby granted to use the JavaScript version under the same
+ * conditions as the YUICompressor (original YUICompressor note below).
+ */
+
+/*
+* YUI Compressor
+* Author: Julien Lecomte - http://www.julienlecomte.net/
+* Copyright (c) 2009 Yahoo! Inc. All rights reserved.
+* The copyrights embodied in the content of this file are licensed
+* by Yahoo! Inc. under the BSD (revised) open source license.
+*/
+var YAHOO = YAHOO || {};
+YAHOO.compressor = YAHOO.compressor || {};
+YAHOO.compressor.cssmin = function (css, linebreakpos){
+
+ var startIndex = 0,
+ endIndex = 0,
+ iemac = false,
+ preserve = false,
+ i = 0, max = 0,
+ preservedTokens = [],
+ token = '';
+
+ // preserve strings so their content doesn't get accidentally minified
+ css = css.replace(/("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g, function(match) {
+ var quote = match[0];
+ preservedTokens.push(match.slice(1, -1));
+ return quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + quote;
+ });
+
+ // Remove all comment blocks...
+ while ((startIndex = css.indexOf("/*", startIndex)) >= 0) {
+ preserve = css.length > startIndex + 2 && css[startIndex + 2] === '!';
+ endIndex = css.indexOf("*/", startIndex + 2);
+ if (endIndex < 0) {
+ if (!preserve) {
+ css = css.slice(0, startIndex);
+ }
+ } else if (endIndex >= startIndex + 2) {
+ if (css[endIndex - 1] === '\\') {
+ // Looks like a comment to hide rules from IE Mac.
+ // Leave this comment, and the following one, but shorten them
+ css = css.slice(0, startIndex) + "/*\\*/" + css.slice(endIndex + 2);
+ startIndex += 5;
+ iemac = true;
+ } else if (iemac && !preserve) {
+ css = css.slice(0, startIndex) + "/**/" + css.slice(endIndex + 2);
+ startIndex += 4;
+ iemac = false;
+ } else if (!preserve) {
+ css = css.slice(0, startIndex) + css.slice(endIndex + 2);
+ } else {
+ // preserve
+ token = css.slice(startIndex+3, endIndex); // 3 is "/*!".length
+ preservedTokens.push(token);
+ css = css.slice(0, startIndex+2) + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + css.slice(endIndex);
+ if (iemac) iemac = false;
+ startIndex += 2;
+ }
+ }
+ }
+
+ // Normalize all whitespace strings to single spaces. Easier to work with that way.
+ css = css.replace(/\s+/g, " ");
+
+ // Remove the spaces before the things that should not have spaces before them.
+ // But, be careful not to turn "p :link {...}" into "p:link{...}"
+ // Swap out any pseudo-class colons with the token, and then swap back.
+ css = css.replace(/(^|\})(([^\{:])+:)+([^\{]*\{)/g, function(m) {
+ return m.replace(":", "___YUICSSMIN_PSEUDOCLASSCOLON___");
+ });
+ css = css.replace(/\s+([!{};:>+\(\)\],])/g, '$1');
+ css = css.replace(/___YUICSSMIN_PSEUDOCLASSCOLON___/g, ":");
+
+ // retain space for special IE6 cases
+ css = css.replace(/:first-(line|letter)({|,)/g, ":first-$1 $2");
+
+ // no space after the end of a preserved comment
+ css = css.replace(/\*\/ /g, '*/');
+
+
+ // If there is a @charset, then only allow one, and push to the top of the file.
+ css = css.replace(/^(.*)(@charset "[^"]*";)/gi, '$2$1');
+ css = css.replace(/^(\s*@charset [^;]+;\s*)+/gi, '$1');
+
+ // Put the space back in some cases, to support stuff like
+ // @media screen and (-webkit-min-device-pixel-ratio:0){
+ css = css.replace(/\band\(/gi, "and (");
+
+
+ // Remove the spaces after the things that should not have spaces after them.
+ css = css.replace(/([!{}:;>+\(\[,])\s+/g, '$1');
+
+ // remove unnecessary semicolons
+ css = css.replace(/;+}/g, "}");
+
+ // Replace 0(px,em,%) with 0.
+ css = css.replace(/([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)/gi, "$1$2");
+
+ // Replace 0 0 0 0; with 0.
+ css = css.replace(/:0 0 0 0;/g, ":0;");
+ css = css.replace(/:0 0 0;/g, ":0;");
+ css = css.replace(/:0 0;/g, ":0;");
+ // Replace background-position:0; with background-position:0 0;
+ css = css.replace(/background-position:0;/gi, "background-position:0 0;");
+
+ // Replace 0.6 to .6, but only when preceded by : or a white-space
+ css = css.replace(/(:|\s)0+\.(\d+)/g, "$1.$2");
+
+ // Shorten colors from rgb(51,102,153) to #336699
+ // This makes it more likely that it'll get further compressed in the next step.
+ css = css.replace(/rgb\s*\(\s*([0-9,\s]+)\s*\)/gi, function(){
+ var rgbcolors = arguments[1].split(',');
+ for (var i = 0; i < rgbcolors.length; i++) {
+ rgbcolors[i] = parseInt(rgbcolors[i], 10).toString(16);
+ if (rgbcolors[i].length === 1) {
+ rgbcolors[i] = '0' + rgbcolors[i];
+ }
+ }
+ return '#' + rgbcolors.join('');
+ });
+
+
+ // Shorten colors from #AABBCC to #ABC. Note that we want to make sure
+ // the color is not preceded by either ", " or =. Indeed, the property
+ // filter: chroma(color="#FFFFFF");
+ // would become
+ // filter: chroma(color="#FFF");
+ // which makes the filter break in IE.
+ css = css.replace(/([^"'=\s])(\s*)#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])/gi, function(){
+ var group = arguments;
+ if (
+ group[3].toLowerCase() === group[4].toLowerCase() &&
+ group[5].toLowerCase() === group[6].toLowerCase() &&
+ group[7].toLowerCase() === group[8].toLowerCase()
+ ) {
+ return (group[1] + group[2] + '#' + group[3] + group[5] + group[7]).toLowerCase();
+ } else {
+ return group[0].toLowerCase();
+ }
+ });
+
+
+ // Remove empty rules.
+ css = css.replace(/[^\};\{\/]+\{\}/g, "");
+
+ if (linebreakpos >= 0) {
+ // Some source control tools don't like it when files containing lines longer
+ // than, say 8000 characters, are checked in. The linebreak option is used in
+ // that case to split long lines after a specific column.
+ startIndex = 0;
+ i = 0;
+ while (i < css.length) {
+ if (css[i++] === '}' && i - startIndex > linebreakpos) {
+ css = css.slice(0, i) + '\n' + css.slice(i);
+ startIndex = i;
+ }
+ }
+ }
+
+ // Replace multiple semi-colons in a row by a single one
+ // See SF bug #1980989
+ css = css.replace(/;;+/g, ";");
+
+ // restore preserved comments and strings
+ for(i = 0, max = preservedTokens.length; i < max; i++) {
+ css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens[i]);
+ }
+
+ // Trim the final string (for any leading or trailing white spaces)
+ css = css.replace(/^\s+|\s+$/g, "");
+
+ return css;
+
+};
+exports.minify = YAHOO.compressor.cssmin;
26 deps/htmlmin.js
@@ -0,0 +1,26 @@
+/*
+Copyright (c) 2009 Mathias Pettersson, mape@mape.me
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+exports.minify = function(html)
+{
+ return html.replace(/>(\n| | )*</g,'><').replace(/[a-z-]+=""/g,'').replace(/"([^ ]*)"/g, '$1').replace(/<\/li>/,'');
+};
370 deps/jsmin.js
@@ -0,0 +1,370 @@
+/*!
+jsmin.js - 2010-01-15
+Author: NanaLich (http://www.cnblogs.com/NanaLich)
+Another patched version for jsmin.js patched by Billy Hoffman,
+this version will try to keep CR LF pairs inside the important comments
+away from being changed into double LF pairs.
+
+jsmin.js - 2009-11-05
+Author: Billy Hoffman
+This is a patched version of jsmin.js created by Franck Marcia which
+supports important comments denoted with /*! ...
+Permission is hereby granted to use the Javascript version under the same
+conditions as the jsmin.js on which it is based.
+
+jsmin.js - 2006-08-31
+Author: Franck Marcia
+This work is an adaptation of jsminc.c published by Douglas Crockford.
+Permission is hereby granted to use the Javascript version under the same
+conditions as the jsmin.c on which it is based.
+
+jsmin.c
+2006-05-04
+
+Copyright (c) 2002 Douglas Crockford (www.crockford.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+Update:
+add level:
+1: minimal, keep linefeeds if single
+2: normal, the standard algorithm
+3: agressive, remove any linefeed and doesn't take care of potential
+missing semicolons (can be regressive)
+store stats
+jsmin.oldSize
+jsmin.newSize
+*/
+
+String.prototype.has = function(c) {
+ return this.indexOf(c) > -1;
+};
+
+function jsmin(comment, input, level) {
+
+ if(input === undefined) {
+ input = comment;
+ comment = '';
+ level = 2;
+ } else if(level === undefined || level < 1 || level > 3) {
+ level = 2;
+ }
+
+ if(comment.length > 0) {
+ comment += '\n';
+ }
+
+ var a = '',
+ b = '',
+ EOF = -1,
+ LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
+ DIGITS = '0123456789',
+ ALNUM = LETTERS + DIGITS + '_$\\',
+ theLookahead = EOF;
+
+
+ /* isAlphanum -- return true if the character is a letter, digit, underscore,
+ dollar sign, or non-ASCII character.
+ */
+
+ function isAlphanum(c) {
+ return c != EOF && (ALNUM.has(c) || c.charCodeAt(0) > 126);
+ }
+
+
+ /* getc(IC) -- return the next character. Watch out for lookahead. If the
+ character is a control character, translate it to a space or
+ linefeed.
+ */
+
+ var iChar = 0, lInput = input.length;
+ function getc() {
+
+ var c = theLookahead;
+ if(iChar == lInput) {
+ return EOF;
+ }
+ theLookahead = EOF;
+ if(c == EOF) {
+ c = input.charAt(iChar);
+ ++iChar;
+ }
+ if(c >= ' ' || c == '\n') {
+ return c;
+ }
+ if(c == '\r') {
+ return '\n';
+ }
+ return ' ';
+ }
+ function getcIC() {
+ var c = theLookahead;
+ if(iChar == lInput) {
+ return EOF;
+ }
+ theLookahead = EOF;
+ if(c == EOF) {
+ c = input.charAt(iChar);
+ ++iChar;
+ }
+ if(c >= ' ' || c == '\n' || c == '\r') {
+ return c;
+ }
+ return ' ';
+ }
+
+
+ /* peek -- get the next character without getting it.
+ */
+
+ function peek() {
+ theLookahead = getc();
+ return theLookahead;
+ }
+
+
+ /* next -- get the next character, excluding comments. peek() is used to see
+ if a '/' is followed by a '/' or '*'.
+ */
+
+ function next() {
+
+ var c = getc();
+ if(c == '/') {
+ switch(peek()) {
+ case '/':
+ for(; ; ) {
+ c = getc();
+ if(c <= '\n') {
+ return c;
+ }
+ }
+ break;
+ case '*':
+ //this is a comment. What kind?
+ getc();
+ if(peek() == '!') {
+ // kill the extra one
+ getc();
+ //important comment
+ var d = '/*!';
+ for(; ; ) {
+ c = getcIC(); // let it know it's inside an important comment
+ switch(c) {
+ case '*':
+ if(peek() == '/') {
+ getc();
+ return d + '*/';
+ }
+ break;
+ case EOF:
+ throw 'Error: Unterminated comment.';
+ default:
+ //modern JS engines handle string concats much better than the
+ //array+push+join hack.
+ d += c;
+ }
+ }
+ } else {
+ //unimportant comment
+ for(; ; ) {
+ switch(getc()) {
+ case '*':
+ if(peek() == '/') {
+ getc();
+ return ' ';
+ }
+ break;
+ case EOF:
+ throw 'Error: Unterminated comment.';
+ }
+ }
+ }
+ break;
+ default:
+ return c;
+ }
+ }
+ return c;
+ }
+
+
+ /* action -- do something! What you do is determined by the argument:
+ 1 Output A. Copy B to A. Get the next B.
+ 2 Copy B to A. Get the next B. (Delete A).
+ 3 Get the next B. (Delete B).
+ action treats a string as a single character. Wow!
+ action recognizes a regular expression if it is preceded by ( or , or =.
+ */
+
+ function action(d) {
+
+ var r = [];
+
+ if(d == 1) {
+ r.push(a);
+ }
+
+ if(d < 3) {
+ a = b;
+ if(a == '\'' || a == '"') {
+ for(; ; ) {
+ r.push(a);
+ a = getc();
+ if(a == b) {
+ break;
+ }
+ if(a <= '\n') {
+ throw 'Error: unterminated string literal: ' + a;
+ }
+ if(a == '\\') {
+ r.push(a);
+ a = getc();
+ }
+ }
+ }
+ }
+
+ b = next();
+
+ if(b == '/' && '(,=:[!&|'.has(a)) {
+ r.push(a);
+ r.push(b);
+ for(; ; ) {
+ a = getc();
+ if(a == '/') {
+ break;
+ } else if(a == '\\') {
+ r.push(a);
+ a = getc();
+ } else if(a <= '\n') {
+ throw 'Error: unterminated Regular Expression literal';
+ }
+ r.push(a);
+ }
+ b = next();
+ }
+
+ return r.join('');
+ }
+
+
+ /* m -- Copy the input to the output, deleting the characters which are
+ insignificant to JavaScript. Comments will be removed. Tabs will be
+ replaced with spaces. Carriage returns will be replaced with
+ linefeeds.
+ Most spaces and linefeeds will be removed.
+ */
+
+ function m() {
+
+ var r = [];
+ a = '\n';
+
+ r.push(action(3));
+
+ while(a != EOF) {
+ switch(a) {
+ case ' ':
+ if(isAlphanum(b)) {
+ r.push(action(1));
+ } else {
+ r.push(action(2));
+ }
+ break;
+ case '\n':
+ switch(b) {
+ case '{':
+ case '[':
+ case '(':
+ case '+':
+ case '-':
+ r.push(action(1));
+ break;
+ case ' ':
+ r.push(action(3));
+ break;
+ default:
+ if(isAlphanum(b)) {
+ r.push(action(1));
+ } else {
+ if(level == 1 && b != '\n') {
+ r.push(action(1));
+ } else {
+ r.push(action(2));
+ }
+ }
+ }
+ break;
+ default:
+ switch(b) {
+ case ' ':
+ if(isAlphanum(a)) {
+ r.push(action(1));
+ break;
+ }
+ r.push(action(3));
+ break;
+ case '\n':
+ if(level == 1 && a != '\n') {
+ r.push(action(1));
+ } else {
+ switch(a) {
+ case '}':
+ case ']':
+ case ')':
+ case '+':
+ case '-':
+ case '"':
+ case '\'':
+ if(level == 3) {
+ r.push(action(3));
+ } else {
+ r.push(action(1));
+ }
+ break;
+ default:
+ if(isAlphanum(a)) {
+ r.push(action(1));
+ } else {
+ r.push(action(3));
+ }
+ }
+ }
+ break;
+ default:
+ r.push(action(1));
+ break;
+ }
+ }
+ }
+
+ return r.join('');
+ }
+
+ jsmin.oldSize = input.length;
+ ret = m(input);
+ jsmin.newSize = ret.length;
+
+ return comment + ret;
+
+}
+exports.minify = jsmin;
74 deps/step/README.markdown
@@ -0,0 +1,74 @@
+# Step
+
+A simple control-flow library for node.JS that makes parallel execution, serial execution, and error handling painless.
+
+## How to install
+
+Simply copy or link the lib/step.js file into your `$HOME/.node_libraries` folder.
+
+## How to use
+
+The step library exports a single function I call `Step`. It accepts any number of functions as arguments and runs them in serial order using the passed in `this` context as the callback to the next step.
+
+ Step(
+ function readSelf() {
+ fs.readFile(__filename, this);
+ },
+ function capitalize(err, text) {
+ if (err) {
+ throw err;
+ }
+ return text.toUpperCase();
+ },
+ function showIt(err, newText) {
+ sys.puts(newText);
+ }
+ );
+
+Notice that we pass in `this` as the callback to `fs.readFile`. When the file read completes, step will send the result as the arguments to the next function in the chain. Then in the `capitalize` function we're doing synchronous work so we can simple return the new value and Step will route it as if we called the callback.
+
+The first parameter is reserved for errors since this is the node standard. Also any exceptions thrown are caught and passed as the first argument to the next function. As long as you don't nest callback functions inline your main functions this prevents there from ever being any uncaught exceptions. This is very important for long running node.JS servers since a single uncaught exception can bring the whole server down.
+
+Also there is support for parallel actions:
+
+ Step(
+ // Loads two files in parallel
+ function loadStuff() {
+ fs.readFile(__filename, this.parallel());
+ fs.readFile("/etc/passwd", this.parallel());
+ },
+ // Show the result when done
+ function showStuff(err, code, users) {
+ if (err) throw err;
+ sys.puts(code);
+ sys.puts(users);
+ }
+ )
+
+Here we pass `this.parallel()` instead of `this` as the callback. It internally keeps track of the number of callbacks issued and preserves their order then giving the result to the next step after all have finished. If there is an error in any of the parallel actions, it will be passed as the first argument to the next step.
+
+Also you can use parallel with a dynamic number of common tasks.
+
+ Step(
+ function readDir() {
+ fs.readdir(__dirname, this);
+ },
+ function readFiles(err, results) {
+ if (err) throw err;
+ // Create a closure to the parallel helper
+ var parallel = this.parallel;
+ results.forEach(function (filename) {
+ if (/\.js$/.test(filename)) {
+ fs.readFile(__dirname + "/" + filename, parallel());
+ }
+ });
+ },
+ function showAll(err /*, file1, file2, ...*/) {
+ if (err) throw err;
+ var files = Array.prototype.slice.call(arguments, 1);
+ sys.p(files);
+ }
+ );
+
+
+Note that we had to create a reference to the `this.parallel` function since the `forEach` creates a new scope and changes `this`. Then using arguments surgery we can extract the contents of the files.
154 deps/step/lib/step.js
@@ -0,0 +1,154 @@
+/*
+Copyright (c) 2010 Tim Caswell <tim@creationix.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+// Inspired by http://github.com/willconant/flow-js, but reimplemented and
+// modified to fit my taste and the node.JS error handling system.
+function Step() {
+ var steps = Array.prototype.slice.call(arguments),
+ counter, results, lock;
+
+ // Define the main callback that's given as `this` to the steps.
+ function next() {
+
+ // Check if there are no steps left
+ if (steps.length === 0) {
+ // Throw uncaught errors
+ if (arguments[0]) {
+ throw arguments[0];
+ }
+ return;
+ }
+
+ // Get the next step to execute
+ var fn = steps.shift();
+ counter = 0;
+ results = [];
+
+ // Run the step in a try..catch block so exceptions don't get out of hand.
+ try {
+ lock = true;
+ var result = fn.apply(next, arguments);
+ } catch (e) {
+ // Pass any exceptions on through the next callback
+ next(e);
+ }
+
+
+ // If a syncronous return is used, pass it to the callback
+ if (result !== undefined) {
+ next(undefined, result);
+ }
+ lock = false;
+ }
+
+ // Add a special callback generator `this.parallel()` that groups stuff.
+ next.parallel = function () {
+ var i = counter;
+ counter++;
+ function check() {
+ counter--;
+ if (counter === 0) {
+ // When they're all done, call the callback
+ next.apply(null, results);
+ }
+ }
+ return function () {
+ // Compress the error from any result to the first argument
+ if (arguments[0]) {
+ results[0] = arguments[0];
+ }
+ // Send the other results as arguments
+ results[i + 1] = arguments[1];
+ if (lock) {
+ process.nextTick(check);
+ return
+ }
+ check();
+ };
+ };
+
+ // Generates a callback generator for grouped results
+ next.group = function () {
+ var localCallback = next.parallel();
+ var counter = 0;
+ var result = [];
+ var error = undefined;
+ // Generates a callback for the group
+ return function () {
+ var i = counter;
+ counter++;
+ function check() {
+ counter--;
+ if (counter === 0) {
+ // When they're all done, call the callback
+ localCallback(error, result);
+ }
+ }
+ return function () {
+ // Compress the error from any result to the first argument
+ if (arguments[0]) {
+ error = arguments[0];
+ }
+ // Send the other results as arguments
+ result[i] = arguments[1];
+ if (lock) {
+ process.nextTick(check);
+ return
+ }
+ check();
+ }
+
+ }
+ };
+
+ // Start the engine an pass nothing to the first step.
+ next([]);
+}
+
+// Tack on leading and tailing steps for input and output and return
+// the whole thing as a function. Basically turns step calls into function
+// factories.
+Step.fn = function StepFn() {
+ var steps = Array.prototype.slice.call(arguments);
+ return function () {
+ var args = Array.prototype.slice.call(arguments);
+
+ // Insert a first step that primes the data stream
+ var toRun = [function () {
+ this.apply(null, args);
+ }].concat(steps);
+
+ // If the last arg is a function add it as a last step
+ if (typeof args[args.length-1] === 'function') {
+ toRun.push(args.pop());
+ }
+
+
+ Step.apply(null, toRun);
+ }
+}
+
+
+// Hook into commonJS module systems
+if (typeof module !== 'undefined' && "exports" in module) {
+ module.exports = Step;
+}
11 deps/step/package.json
@@ -0,0 +1,11 @@
+{ "name": "step",
+ "version": "0.0.3",
+ "description": "A simple control-flow library for node.JS that makes parallel execution, serial execution, and error handling painless.",
+ "engine": [ "node >=0.1.90" ],
+ "author": "Tim Caswell <tim@creationix.com>",
+ "repository":
+ { "type" : "git",
+ "url" : "http://github.com/creationix/step.git"
+ },
+ "main": "lib/step"
+}
80 deps/step/test.js
@@ -0,0 +1,80 @@
+var Step = require(__dirname + "/lib/step"),
+ fs = require('fs'),
+ sys = require('sys');
+
+Step(
+ function () {
+ var group = this.group();
+ [1,2,3,4,5,6].forEach(function (num) {
+ fs.readFile(__filename, group());
+ });
+ },
+ function (err, contents) {
+ if (err) { throw err; }
+ sys.p(contents);
+ }
+);
+
+Step(
+ function readSelf() {
+ fs.readFile(__filename, this);
+ },
+ function capitalize(err, text) {
+ if (err) {
+ throw err;
+ }
+ return text.toUpperCase();
+ },
+ function showIt(err, newText) {
+ sys.puts(newText);
+ }
+);
+
+
+Step(
+ // Loads two files in parallel
+ function loadStuff() {
+ fs.readFile(__filename, this.parallel());
+ fs.readFile("/etc/passwd", this.parallel());
+ },
+ // Show the result when done
+ function showStuff(err, code, users) {
+ if (err) throw err;
+ sys.puts(code);
+ sys.puts(users);
+ }
+)
+
+Step(
+ function readDir() {
+ fs.readdir(__dirname, this);
+ },
+ function readFiles(err, results) {
+ if (err) throw err;
+ // Create a closure to the parallel helper
+ var parallel = this.parallel;
+ results.forEach(function (filename) {
+ if (/\.js$/.test(filename)) {
+ fs.readFile(__dirname + "/" + filename, parallel());
+ }
+ });
+ },
+ function showAll(err /*, file1, file2, ...*/) {
+ if (err) throw err;
+ var files = Array.prototype.slice.call(arguments, 1);
+ sys.p(files);
+ }
+);
+
+var myfn = Step.fn(
+ function (name) {
+ fs.readFile(name, this);
+ },
+ function capitalize(err, text) {
+ if (err) {
+ throw err;
+ }
+ return text.toUpperCase();
+ }
+);
+myfn(__filename, sys.p);
233 lib/assetmanager.js
@@ -0,0 +1,233 @@
+var sys = require('sys'),
+ fs = require('fs'),
+ Buffer = require('buffer').Buffer,
+ Step = require('./../deps/step/lib/step'),
+ jsmin = require('./../deps/jsmin').minify,
+ htmlmin = require('./../deps/htmlmin').minify,
+ cssmin = require('./../deps/cssmin').minify;
+var cache = {};
+
+module.exports = function assetManager (settings)
+{
+ var self = this;
+ this.cacheTimestamps = {};
+
+ this.generateCache = function (generateGroup) {
+ var self = this;
+ settings.forEach(function (group, groupName) {
+ var userAgentMatches = [];
+ if (group.preManipulate)
+ {
+ Object.keys(group.preManipulate).forEach(function(key) {
+ userAgentMatches.push(key);
+ });
+ }
+ if (group.postManipulate)
+ {
+ Object.keys(group.postManipulate).forEach(function(key) {
+ userAgentMatches.push(key);
+ });
+ }
+ if (!userAgentMatches.length)
+ {
+ userAgentMatches = ['^'];
+ }
+
+ userAgentMatches.forEach(function(match) {
+ var path = group.path;
+ Step(function () {
+ var grouping = this.group();
+ group.files.forEach(function (file) {
+ if (!generateGroup || generateGroup && groupName == generateGroup) {
+ self.getFile(path + file, groupName, grouping());
+ }
+ });
+ }, function (err, contents) {
+ if (err) {
+ throw err;
+ }
+ var grouping = this.group();
+ var lastModified = null;
+
+ for (var i = 0, l = contents.length; i < l; i++) {
+ var file = contents[i];
+ if (!lastModified || lastModified.getTime() < file.modified.getTime()) {
+ lastModified = file.modified;
+ }
+ if (!group.preManipulate) {
+ group.preManipulate = {};
+ }
+ self.manipulate(group.preManipulate[match], file.content, file.filePath, i, i === l - 1, grouping());
+ };
+ self.cacheTimestamps[groupName] = lastModified.getTime();
+ if (!cache[groupName]) {
+ cache[groupName] = {};
+ }
+ cache[groupName][match] = {
+ 'modified': lastModified.toUTCString()
+ };
+ }, function (err, contents) {
+ if (err) {
+ throw err;
+ }
+ var grouping = this.group();
+
+ var content = '';
+ for (var i=0; i < contents.length; i++) {
+ content += contents[i];
+ };
+
+ if (!group.debug) {
+ if (group.dataType.toLowerCase() === 'javascript') {
+ (function (callback){callback(null, jsmin(content));})(grouping());
+ }
+ else if (group.dataType.toLowerCase() === 'html') {
+ (function (callback){callback(null, htmlmin(content));})(grouping());
+ }
+ else if (group.dataType.toLowerCase() === 'css') {
+ (function (callback){callback(null, cssmin(content));})(grouping());
+ }
+ } else {
+ (function (callback){callback(null, content);})(grouping());
+ }
+ }, function (err, contents) {
+ if (err) {
+ throw err;
+ }
+ var grouping = this.group();
+
+ var content = '';
+ for (var i=0; i < contents.length; i++) {
+ content += contents[i];
+ };
+
+ if (!group.postManipulate) {
+ group.postManipulate = {};
+ }
+ self.manipulate(group.postManipulate[match], content, null, 0, true, grouping());
+
+ }, function (err, contents) {
+ if (err) {
+ throw err;
+ }
+
+ var content = '';
+ for (var i=0; i < contents.length; i++) {
+ content += contents[i];
+ };
+
+ cache[groupName][match].contentBuffer = new Buffer(content, 'utf8');
+ cache[groupName][match].contentLenght = cache[groupName][match].contentBuffer.length;
+ });
+ });
+ });
+ };
+ this.manipulate = function (manipulateInstructions, fileContent, path, index, last, callback) {
+ if (manipulateInstructions && typeof manipulateInstructions == 'object' && manipulateInstructions.length) {
+ var callIndex = -1;
+
+ var modify = function (content, path, index, last, modify)
+ {
+ if (callIndex < manipulateInstructions.length-1) {
+ callIndex++;
+
+ manipulateInstructions[callIndex](content, path, index, last, function (content) {
+ modify(content, path, index, last, modify);
+ });
+ } else {
+ callback(null, content);
+ }
+ };
+ modify(fileContent, path, index, last, modify);
+ } else if (manipulateInstructions && typeof manipulateInstructions == 'function') {
+ manipulateInstructions(fileContent, path, index, last, callback);
+ } else {
+ callback(null, fileContent);
+ }
+ };
+ this.getFile = function (filePath, groupName, callback) {
+ setTimeout(function () {
+ var fileInfo = {
+ 'filePath': filePath
+ };
+ fs.readFile(filePath, function (err, data) {
+
+ if (err) {
+ throw err;
+ }
+ fileInfo.content = data.toString();
+ fs.stat(filePath, function (err, stat) {
+ fileInfo.modified = stat.mtime;
+ callback(null, fileInfo);
+ });
+ });
+ }, 100);
+ };
+
+ this.generateCache();
+
+ settings.forEach(function (group, groupName) {
+ if (!group.stale) {
+ group.files.forEach(function (file) {
+ fs.watchFile(group.path + file, function (old, newFile) {
+ if (old.mtime.toString() != newFile.mtime.toString()) {
+ self.generateCache(groupName);
+ }
+ });
+ });
+ }
+ });
+
+ function assetManager (req, res, next)
+ {
+ var self = this;
+ var found = false;
+ var responce = {};
+ var mimeType = 'text/plain';
+
+ settings.forEach(function (group, groupName) {
+ if (group.route.test(req.url)) {
+ var userAgent = req.headers['user-agent'];
+
+ if (group.dataType == 'javascript') {
+ mimeType = 'application/javascript';
+ }
+ else if (group.dataType == 'html') {
+ mimeType = 'text/html';
+ }
+ else if (group.dataType == 'css') {
+ mimeType = 'text/css';
+ }
+ Object.keys(cache[groupName]).forEach(function(match) {
+ if (!found && userAgent.match(new RegExp(match, 'i'))) {
+ found = true;
+ responce = {
+ contentLenght: cache[groupName][match].contentLenght
+ , modified: cache[groupName][match].modified
+ , contentBuffer: cache[groupName][match].contentBuffer
+ };
+ }
+ });
+ }
+ });
+
+ if (!found) {
+ next();
+ } else {
+ res.writeHead(200, {
+ 'Content-Type': mimeType,
+ 'Content-Length': responce.contentLenght,
+ 'Last-Modified': responce.modified,
+ 'Date': (new Date).toUTCString(),
+ 'Cache-Control': 'public max-age=' + 31536000,
+ 'Expires': (new Date(new Date().getTime()+63113852000)).toUTCString()
+ });
+ res.end(responce.contentBuffer);
+
+ return;
+ }
+ };
+
+ assetManager.cacheTimestamps = this.cacheTimestamps;
+ return assetManager;
+};

0 comments on commit 15e3799

Please sign in to comment.
Something went wrong with that request. Please try again.