├── LICENSE ├── README.md ├── deps ├── cssmin.js ├── htmlmin.js └── jsmin.js ├── lib └── assetmanager.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Mathias Pettersson, mape@mape.me 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # connect-assetmanager 2 | 3 | Middleware for Connect (node.js) for handling your static assets. 4 | 5 | 6 | 7 | ## Installation 8 | 9 | Via [npm](http://github.com/isaacs/npm): 10 | 11 | $ npm install connect-assetmanager 12 | 13 | ## Handy pre/post hooks 14 | 15 | Make sure to check out [connect-assetmanager-handlers](http://github.com/mape/connect-assetmanager-handlers) for useful hooks you can use (inline base64 for image, vendor prefix fixes for example) 16 | 17 | ## What does it allow you to do? 18 | * Merge and minify CSS/javascript files 19 | * Auto regenerates the cache on file change so no need for restart of server or manual action. 20 | * Run pre/post manipulation on the files 21 | * __Use regex to match user agent so you can serve different modified versions of your packed assets based on the requesting browser.__ 22 | * Supplies a reference to the modified dates for all groups through assetManager().cacheTimestamps[groupName] as well as md5 hashes assetManager().cacheHashes[groupName] which can be used for cache invalidation in templates. 23 | * Wildcard add files from dir 24 | 25 | ### Nifty things you can do with the pre/post manipulation 26 | * __Replace all url(references to images) with inline base64 data which remove all would be image HTTP requests.__ 27 | * Strip all IE specific code for all other browsers. 28 | * Fix all the vendor prefixes (-ms -moz -webkit -o) for things like border-radius instead of having to type all each and every time. 29 | 30 | ## Speed test (it does just fine) 31 | ### Running with 32 | > connect app -n 4 33 | 34 | ### Common data 35 | Concurrency Level: 240 36 | Complete requests: 10000 37 | Failed requests: 0 38 | Write errors: 0 39 | 40 | ### Small (reset.css) 41 | Document Path: /static/test/small 42 | Document Length: 170 bytes 43 | 44 | Time taken for tests: 0.588 seconds 45 | Total transferred: 4380001 bytes 46 | HTML transferred: 1700000 bytes 47 | Requests per second: 17005.50 [#/sec] (mean) 48 | Time per request: 14.113 [ms] (mean) 49 | Time per request: 0.059 [ms] (mean, across all concurrent requests) 50 | Transfer rate: 7273.84 [Kbytes/sec] received 51 | 52 | ### Larger (jQuery.js) 53 | Document Path: /static/test/large 54 | Document Length: 100732 bytes 55 | 56 | Time taken for tests: 10.817 seconds 57 | Total transferred: 1012772490 bytes 58 | HTML transferred: 1009913368 bytes 59 | Requests per second: 924.51 [#/sec] (mean) 60 | Time per request: 259.597 [ms] (mean) 61 | Time per request: 1.082 [ms] (mean, across all concurrent requests) 62 | Transfer rate: 91437.43 [Kbytes/sec] received 63 | 64 | ## Options 65 | ### path (string) - required 66 | The path to the folder containing the files. 67 | 68 | path: __dirname + '/' 69 | 70 | ### files (array) - required 71 | An array of strings containing the filenames of all files in the group. 72 | 73 | If you want to add all files from the path supplied add '*'. It will insert the files at the position of the *. 74 | You can also use a regexp to match files or use external urls. 75 | 76 | files: ['http://code.jquery.com/jquery-latest.js', /jquery.*/ , '*', 'page.js'] 77 | 78 | ### route (regex as string) - required 79 | The route that will be matched by Connect. 80 | 81 | route: '/\/assets\/css\/.*\.css' 82 | 83 | ### dataType (string), ['javascript', 'css'] 84 | 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. 85 | 86 | dataType: 'css' 87 | 88 | ### preManipulate (array containing functions) 89 | There are hooks in the assetManager that allow you to programmaticly alter the source of the files you are grouping. 90 | 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. 91 | 92 | 'preManipulate': { 93 | // Regexp to match user-agents including MSIE. 94 | 'MSIE': [ 95 | generalManipulation 96 | , msieSpecificManipulation 97 | ], 98 | // Matches all (regex start line) 99 | '^': [ 100 | generalManipulation 101 | , fixVendorPrefixes 102 | , fixGradients 103 | , replaceImageRefToBase64 104 | ] 105 | } 106 | 107 | ### postManipulate (array containing functions) 108 | Same as preManipulate but runs after the files are merged and minified. 109 | 110 | The functions supplied look like this: 111 | 112 | function (file, path, index, isLast, callback) { 113 | if (path.match(/filename\.js/)) { 114 | callback(null, file.replace(/string/mig, 'replaceWithThis')); 115 | } else { 116 | callback(null, file); 117 | } 118 | } 119 | ### serveModify (req, res, response, callback) 120 | Allows you do to modify the cached response on a per request basis. 121 | 122 | function(req, res, response, callback) { 123 | if (externalVariable) { 124 | // Return empty asset 125 | response.length = 1; 126 | response.contentBuffer = new Buffer(' '); 127 | } 128 | callback(response); 129 | } 130 | ### stale (boolean) 131 | Incase you want to use the asset manager with optimal performance you can set stale to true. 132 | 133 | This means that there are no checks for file changes and the cache will therefore not be regenerated. Recommended for deployed code. 134 | 135 | ### debug (boolean) 136 | When debug is set to true the files will not be minified, but they will be grouped into one file and modified. 137 | 138 | ## Example usage 139 | var sys = require('sys'); 140 | var fs = require('fs'); 141 | var Connect = require('connect'); 142 | var assetManager = require('connect-assetmanager'); 143 | var assetHandler = require('connect-assetmanager-handlers'); 144 | 145 | var root = __dirname + '/public'; 146 | 147 | 148 | var Server = module.exports = Connect.createServer(); 149 | 150 | Server.use('/', 151 | Connect.responseTime() 152 | , Connect.logger() 153 | ); 154 | 155 | var assetManagerGroups = { 156 | 'js': { 157 | 'route': /\/static\/js\/[0-9]+\/.*\.js/ 158 | , 'path': './public/js/' 159 | , 'dataType': 'javascript' 160 | , 'files': [ 161 | 'jquery.js' 162 | , 'jquery.client.js' 163 | ] 164 | }, 'css': { 165 | 'route': /\/static\/css\/[0-9]+\/.*\.css/ 166 | , 'path': './public/css/' 167 | , 'dataType': 'css' 168 | , 'files': [ 169 | 'reset.css' 170 | , 'style.css' 171 | ] 172 | , 'preManipulate': { 173 | // Regexp to match user-agents including MSIE. 174 | 'MSIE': [ 175 | assetHandler.yuiCssOptimize 176 | , assetHandler.fixVendorPrefixes 177 | , assetHandler.fixGradients 178 | , assetHandler.stripDataUrlsPrefix 179 | ], 180 | // Matches all (regex start line) 181 | '^': [ 182 | assetHandler.yuiCssOptimize 183 | , assetHandler.fixVendorPrefixes 184 | , assetHandler.fixGradients 185 | , assetHandler.replaceImageRefToBase64(root) 186 | ] 187 | } 188 | } 189 | }; 190 | 191 | var assetsManagerMiddleware = assetManager(assetManagerGroups); 192 | Server.use('/' 193 | , assetsManagerMiddleware 194 | , Connect.static(root) 195 | ); 196 | -------------------------------------------------------------------------------- /deps/cssmin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cssmin.js 3 | * Author: Stoyan Stefanov - http://phpied.com/ 4 | * This is a JavaScript port of the CSS minification tool 5 | * distributed with YUICompressor, itself a port 6 | * of the cssmin utility by Isaac Schlueter - http://foohack.com/ 7 | * Permission is hereby granted to use the JavaScript version under the same 8 | * conditions as the YUICompressor (original YUICompressor note below). 9 | */ 10 | 11 | /* 12 | * YUI Compressor 13 | * Author: Julien Lecomte - http://www.julienlecomte.net/ 14 | * Copyright (c) 2009 Yahoo! Inc. All rights reserved. 15 | * The copyrights embodied in the content of this file are licensed 16 | * by Yahoo! Inc. under the BSD (revised) open source license. 17 | */ 18 | var YAHOO = YAHOO || {}; 19 | YAHOO.compressor = YAHOO.compressor || {}; 20 | YAHOO.compressor.cssmin = function (css, linebreakpos){ 21 | 22 | var startIndex = 0, 23 | endIndex = 0, 24 | iemac = false, 25 | preserve = false, 26 | i = 0, max = 0, 27 | preservedTokens = [], 28 | token = ''; 29 | 30 | // preserve strings so their content doesn't get accidentally minified 31 | css = css.replace(/("([^\\"\n]|\\.|\\)*")|('([^\\'\n]|\\.|\\)*')/g, function(match) { 32 | var quote = match[0]; 33 | preservedTokens.push(match.slice(1, -1)); 34 | return quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + quote; 35 | }); 36 | 37 | // Remove all comment blocks... 38 | while ((startIndex = css.indexOf("/*", startIndex)) >= 0) { 39 | preserve = css.length > startIndex + 2 && css[startIndex + 2] === '!'; 40 | endIndex = css.indexOf("*/", startIndex + 2); 41 | if (endIndex < 0) { 42 | if (!preserve) { 43 | css = css.slice(0, startIndex); 44 | } 45 | } else if (endIndex >= startIndex + 2) { 46 | if (css[endIndex - 1] === '\\') { 47 | // Looks like a comment to hide rules from IE Mac. 48 | // Leave this comment, and the following one, but shorten them 49 | css = css.slice(0, startIndex) + "/*\\*/" + css.slice(endIndex + 2); 50 | startIndex += 5; 51 | iemac = true; 52 | } else if (iemac && !preserve) { 53 | css = css.slice(0, startIndex) + "/**/" + css.slice(endIndex + 2); 54 | startIndex += 4; 55 | iemac = false; 56 | } else if (!preserve) { 57 | css = css.slice(0, startIndex) + css.slice(endIndex + 2); 58 | } else { 59 | // preserve 60 | token = css.slice(startIndex+3, endIndex); // 3 is "/*!".length 61 | preservedTokens.push(token); 62 | css = css.slice(0, startIndex+2) + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + css.slice(endIndex); 63 | if (iemac) iemac = false; 64 | startIndex += 2; 65 | } 66 | } 67 | } 68 | 69 | // Normalize all whitespace strings to single spaces. Easier to work with that way. 70 | css = css.replace(/\s+/g, " "); 71 | 72 | // Remove the spaces before the things that should not have spaces before them. 73 | // But, be careful not to turn "p :link {...}" into "p:link{...}" 74 | // Swap out any pseudo-class colons with the token, and then swap back. 75 | css = css.replace(/(^|\})(([^\{:])+:)+([^\{]*\{)/g, function(m) { 76 | return m.replace(":", "___YUICSSMIN_PSEUDOCLASSCOLON___"); 77 | }); 78 | css = css.replace(/\s+([!{};:>+\(\)\],])/g, '$1'); 79 | css = css.replace(/___YUICSSMIN_PSEUDOCLASSCOLON___/g, ":"); 80 | 81 | // retain space for special IE6 cases 82 | css = css.replace(/:first-(line|letter)({|,)/g, ":first-$1 $2"); 83 | 84 | // no space after the end of a preserved comment 85 | css = css.replace(/\*\/ /g, '*/'); 86 | 87 | 88 | // If there is a @charset, then only allow one, and push to the top of the file. 89 | css = css.replace(/^(.*)(@charset "[^"]*";)/gi, '$2$1'); 90 | css = css.replace(/^(\s*@charset [^;]+;\s*)+/gi, '$1'); 91 | 92 | // Put the space back in some cases, to support stuff like 93 | // @media screen and (-webkit-min-device-pixel-ratio:0){ 94 | css = css.replace(/\band\(/gi, "and ("); 95 | 96 | 97 | // Remove the spaces after the things that should not have spaces after them. 98 | css = css.replace(/([!{}:;>+\(\[,])\s+/g, '$1'); 99 | 100 | // remove unnecessary semicolons 101 | css = css.replace(/;+}/g, "}"); 102 | 103 | // Replace 0(px,em,%) with 0. 104 | css = css.replace(/([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)/gi, "$1$2"); 105 | 106 | // Replace 0 0 0 0; with 0. 107 | css = css.replace(/:0 0 0 0;/g, ":0;"); 108 | css = css.replace(/:0 0 0;/g, ":0;"); 109 | css = css.replace(/:0 0;/g, ":0;"); 110 | // Replace background-position:0; with background-position:0 0; 111 | css = css.replace(/background-position:0;/gi, "background-position:0 0;"); 112 | 113 | // Replace 0.6 to .6, but only when preceded by : or a white-space 114 | css = css.replace(/(:|\s)0+\.(\d+)/g, "$1.$2"); 115 | 116 | // Shorten colors from rgb(51,102,153) to #336699 117 | // This makes it more likely that it'll get further compressed in the next step. 118 | css = css.replace(/rgb\s*\(\s*([0-9,\s]+)\s*\)/gi, function(){ 119 | var rgbcolors = arguments[1].split(','); 120 | for (var i = 0; i < rgbcolors.length; i++) { 121 | rgbcolors[i] = parseInt(rgbcolors[i], 10).toString(16); 122 | if (rgbcolors[i].length === 1) { 123 | rgbcolors[i] = '0' + rgbcolors[i]; 124 | } 125 | } 126 | return '#' + rgbcolors.join(''); 127 | }); 128 | 129 | 130 | // Shorten colors from #AABBCC to #ABC. Note that we want to make sure 131 | // the color is not preceded by either ", " or =. Indeed, the property 132 | // filter: chroma(color="#FFFFFF"); 133 | // would become 134 | // filter: chroma(color="#FFF"); 135 | // which makes the filter break in IE. 136 | 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(){ 137 | var group = arguments; 138 | if ( 139 | group[3].toLowerCase() === group[4].toLowerCase() && 140 | group[5].toLowerCase() === group[6].toLowerCase() && 141 | group[7].toLowerCase() === group[8].toLowerCase() 142 | ) { 143 | return (group[1] + group[2] + '#' + group[3] + group[5] + group[7]).toLowerCase(); 144 | } else { 145 | return group[0].toLowerCase(); 146 | } 147 | }); 148 | 149 | 150 | // Remove empty rules. 151 | css = css.replace(/[^\};\{\/]+\{\}/g, ""); 152 | 153 | if (linebreakpos >= 0) { 154 | // Some source control tools don't like it when files containing lines longer 155 | // than, say 8000 characters, are checked in. The linebreak option is used in 156 | // that case to split long lines after a specific column. 157 | startIndex = 0; 158 | i = 0; 159 | while (i < css.length) { 160 | if (css[i++] === '}' && i - startIndex > linebreakpos) { 161 | css = css.slice(0, i) + '\n' + css.slice(i); 162 | startIndex = i; 163 | } 164 | } 165 | } 166 | 167 | // Replace multiple semi-colons in a row by a single one 168 | // See SF bug #1980989 169 | css = css.replace(/;;+/g, ";"); 170 | 171 | // restore preserved comments and strings 172 | for(i = 0, max = preservedTokens.length; i < max; i++) { 173 | css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens[i]); 174 | } 175 | 176 | // Trim the final string (for any leading or trailing white spaces) 177 | css = css.replace(/^\s+|\s+$/g, ""); 178 | 179 | return css; 180 | 181 | }; 182 | exports.minify = YAHOO.compressor.cssmin; 183 | -------------------------------------------------------------------------------- /deps/htmlmin.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2009 Mathias Pettersson, mape@mape.me 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | exports.minify = function(html) 24 | { 25 | return html.replace(/>(\n| | )*<').replace(/[a-z-]+=""/g,'').replace(/"([^ ]*)"/g, '$1').replace(/<\/li>/,''); 26 | }; -------------------------------------------------------------------------------- /deps/jsmin.js: -------------------------------------------------------------------------------- 1 | /*! 2 | jsmin.js - 2010-01-15 3 | Author: NanaLich (http://www.cnblogs.com/NanaLich) 4 | Another patched version for jsmin.js patched by Billy Hoffman, 5 | this version will try to keep CR LF pairs inside the important comments 6 | away from being changed into double LF pairs. 7 | 8 | jsmin.js - 2009-11-05 9 | Author: Billy Hoffman 10 | This is a patched version of jsmin.js created by Franck Marcia which 11 | supports important comments denoted with /*! ... 12 | Permission is hereby granted to use the Javascript version under the same 13 | conditions as the jsmin.js on which it is based. 14 | 15 | jsmin.js - 2006-08-31 16 | Author: Franck Marcia 17 | This work is an adaptation of jsminc.c published by Douglas Crockford. 18 | Permission is hereby granted to use the Javascript version under the same 19 | conditions as the jsmin.c on which it is based. 20 | 21 | jsmin.c 22 | 2006-05-04 23 | 24 | Copyright (c) 2002 Douglas Crockford (www.crockford.com) 25 | 26 | Permission is hereby granted, free of charge, to any person obtaining a copy of 27 | this software and associated documentation files (the "Software"), to deal in 28 | the Software without restriction, including without limitation the rights to 29 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 30 | of the Software, and to permit persons to whom the Software is furnished to do 31 | so, subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in all 34 | copies or substantial portions of the Software. 35 | 36 | The Software shall be used for Good, not Evil. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 | SOFTWARE. 45 | 46 | Update: 47 | add level: 48 | 1: minimal, keep linefeeds if single 49 | 2: normal, the standard algorithm 50 | 3: agressive, remove any linefeed and doesn't take care of potential 51 | missing semicolons (can be regressive) 52 | store stats 53 | jsmin.oldSize 54 | jsmin.newSize 55 | */ 56 | 57 | String.prototype.has = function(c) { 58 | return this.indexOf(c) > -1; 59 | }; 60 | 61 | function jsmin(comment, input, level) { 62 | 63 | if(input === undefined) { 64 | input = comment; 65 | comment = ''; 66 | level = 2; 67 | } else if(level === undefined || level < 1 || level > 3) { 68 | level = 2; 69 | } 70 | 71 | if(comment.length > 0) { 72 | comment += '\n'; 73 | } 74 | 75 | var a = '', 76 | b = '', 77 | EOF = -1, 78 | LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 79 | DIGITS = '0123456789', 80 | ALNUM = LETTERS + DIGITS + '_$\\', 81 | theLookahead = EOF; 82 | 83 | 84 | /* isAlphanum -- return true if the character is a letter, digit, underscore, 85 | dollar sign, or non-ASCII character. 86 | */ 87 | 88 | function isAlphanum(c) { 89 | return c != EOF && (ALNUM.has(c) || c.charCodeAt(0) > 126); 90 | } 91 | 92 | 93 | /* getc(IC) -- return the next character. Watch out for lookahead. If the 94 | character is a control character, translate it to a space or 95 | linefeed. 96 | */ 97 | 98 | var iChar = 0, lInput = input.length; 99 | function getc() { 100 | 101 | var c = theLookahead; 102 | if(iChar == lInput) { 103 | return EOF; 104 | } 105 | theLookahead = EOF; 106 | if(c == EOF) { 107 | c = input.charAt(iChar); 108 | ++iChar; 109 | } 110 | if(c >= ' ' || c == '\n') { 111 | return c; 112 | } 113 | if(c == '\r') { 114 | return '\n'; 115 | } 116 | return ' '; 117 | } 118 | function getcIC() { 119 | var c = theLookahead; 120 | if(iChar == lInput) { 121 | return EOF; 122 | } 123 | theLookahead = EOF; 124 | if(c == EOF) { 125 | c = input.charAt(iChar); 126 | ++iChar; 127 | } 128 | if(c >= ' ' || c == '\n' || c == '\r') { 129 | return c; 130 | } 131 | return ' '; 132 | } 133 | 134 | 135 | /* peek -- get the next character without getting it. 136 | */ 137 | 138 | function peek() { 139 | theLookahead = getc(); 140 | return theLookahead; 141 | } 142 | 143 | 144 | /* next -- get the next character, excluding comments. peek() is used to see 145 | if a '/' is followed by a '/' or '*'. 146 | */ 147 | 148 | function next() { 149 | 150 | var c = getc(); 151 | if(c == '/') { 152 | switch(peek()) { 153 | case '/': 154 | for(; ; ) { 155 | c = getc(); 156 | if(c <= '\n') { 157 | return c; 158 | } 159 | } 160 | break; 161 | case '*': 162 | //this is a comment. What kind? 163 | getc(); 164 | if(peek() == '!') { 165 | // kill the extra one 166 | getc(); 167 | //important comment 168 | var d = '/*!'; 169 | for(; ; ) { 170 | c = getcIC(); // let it know it's inside an important comment 171 | switch(c) { 172 | case '*': 173 | if(peek() == '/') { 174 | getc(); 175 | return d + '*/'; 176 | } 177 | break; 178 | case EOF: 179 | throw 'Error: Unterminated comment.'; 180 | default: 181 | //modern JS engines handle string concats much better than the 182 | //array+push+join hack. 183 | d += c; 184 | } 185 | } 186 | } else { 187 | //unimportant comment 188 | for(; ; ) { 189 | switch(getc()) { 190 | case '*': 191 | if(peek() == '/') { 192 | getc(); 193 | return ' '; 194 | } 195 | break; 196 | case EOF: 197 | throw 'Error: Unterminated comment.'; 198 | } 199 | } 200 | } 201 | break; 202 | default: 203 | return c; 204 | } 205 | } 206 | return c; 207 | } 208 | 209 | 210 | /* action -- do something! What you do is determined by the argument: 211 | 1 Output A. Copy B to A. Get the next B. 212 | 2 Copy B to A. Get the next B. (Delete A). 213 | 3 Get the next B. (Delete B). 214 | action treats a string as a single character. Wow! 215 | action recognizes a regular expression if it is preceded by ( or , or =. 216 | */ 217 | 218 | function action(d) { 219 | 220 | var r = []; 221 | 222 | if(d == 1) { 223 | r.push(a); 224 | } 225 | 226 | if(d < 3) { 227 | a = b; 228 | if(a == '\'' || a == '"') { 229 | for(; ; ) { 230 | r.push(a); 231 | a = getc(); 232 | if(a == b) { 233 | break; 234 | } 235 | if(a <= '\n') { 236 | throw 'Error: unterminated string literal: ' + a; 237 | } 238 | if(a == '\\') { 239 | r.push(a); 240 | a = getc(); 241 | } 242 | } 243 | } 244 | } 245 | 246 | b = next(); 247 | 248 | if(b == '/' && '(,=:[!&|'.has(a)) { 249 | r.push(a); 250 | r.push(b); 251 | for(; ; ) { 252 | a = getc(); 253 | if(a == '/') { 254 | break; 255 | } else if(a == '\\') { 256 | r.push(a); 257 | a = getc(); 258 | } else if(a <= '\n') { 259 | throw 'Error: unterminated Regular Expression literal'; 260 | } 261 | r.push(a); 262 | } 263 | b = next(); 264 | } 265 | 266 | return r.join(''); 267 | } 268 | 269 | 270 | /* m -- Copy the input to the output, deleting the characters which are 271 | insignificant to JavaScript. Comments will be removed. Tabs will be 272 | replaced with spaces. Carriage returns will be replaced with 273 | linefeeds. 274 | Most spaces and linefeeds will be removed. 275 | */ 276 | 277 | function m() { 278 | 279 | var r = []; 280 | a = '\n'; 281 | 282 | r.push(action(3)); 283 | 284 | while(a != EOF) { 285 | switch(a) { 286 | case ' ': 287 | if(isAlphanum(b)) { 288 | r.push(action(1)); 289 | } else { 290 | r.push(action(2)); 291 | } 292 | break; 293 | case '\n': 294 | switch(b) { 295 | case '{': 296 | case '[': 297 | case '(': 298 | case '+': 299 | case '-': 300 | r.push(action(1)); 301 | break; 302 | case ' ': 303 | r.push(action(3)); 304 | break; 305 | default: 306 | if(isAlphanum(b)) { 307 | r.push(action(1)); 308 | } else { 309 | if(level == 1 && b != '\n') { 310 | r.push(action(1)); 311 | } else { 312 | r.push(action(2)); 313 | } 314 | } 315 | } 316 | break; 317 | default: 318 | switch(b) { 319 | case ' ': 320 | if(isAlphanum(a)) { 321 | r.push(action(1)); 322 | break; 323 | } 324 | r.push(action(3)); 325 | break; 326 | case '\n': 327 | if(level == 1 && a != '\n') { 328 | r.push(action(1)); 329 | } else { 330 | switch(a) { 331 | case '}': 332 | case ']': 333 | case ')': 334 | case '+': 335 | case '-': 336 | case '"': 337 | case '\'': 338 | if(level == 3) { 339 | r.push(action(3)); 340 | } else { 341 | r.push(action(1)); 342 | } 343 | break; 344 | default: 345 | if(isAlphanum(a)) { 346 | r.push(action(1)); 347 | } else { 348 | r.push(action(3)); 349 | } 350 | } 351 | } 352 | break; 353 | default: 354 | r.push(action(1)); 355 | break; 356 | } 357 | } 358 | } 359 | 360 | return r.join(''); 361 | } 362 | 363 | jsmin.oldSize = input.length; 364 | ret = m(input); 365 | jsmin.newSize = ret.length; 366 | 367 | return comment + ret; 368 | 369 | } 370 | exports.minify = jsmin; -------------------------------------------------------------------------------- /lib/assetmanager.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , Buffer = require('buffer').Buffer 3 | , request = require('request') 4 | , Step = require('step') 5 | , jsmin = require('./../deps/jsmin').minify 6 | , htmlmin = require('./../deps/htmlmin').minify 7 | , cssmin = require('./../deps/cssmin').minify 8 | , crypto = require('crypto'); 9 | 10 | var zlib; 11 | try { 12 | zlib = require('zlib'); 13 | } catch(e) {} 14 | 15 | var cache = {} 16 | , settings = {} 17 | , cacheHashes = {} 18 | , cacheTimestamps = {}; 19 | 20 | module.exports = function assetManager (assets) { 21 | var self = this; 22 | 23 | settings = assets || settings; 24 | if (!settings) { 25 | throw new Exception('No asset groups found'); 26 | } 27 | 28 | if (!settings.forEach) { 29 | settings.forEach = function(callback) { 30 | Object.keys(this).forEach(function(key) { 31 | if (key !== 'forEach') { 32 | callback(settings[key], key); 33 | } 34 | }); 35 | }; 36 | } 37 | 38 | Step(function() { 39 | var grouping = this.group(); 40 | settings.forEach(function(group, groupName) { 41 | var patterns = [] 42 | , insertions = [] 43 | , matchInsertionCount = {}; 44 | 45 | group.files.forEach(function(fileName, index) { 46 | var pattern = null; 47 | if (fileName.exec) { // Got a RegEx 48 | pattern = fileName; 49 | } else if (fileName.trim() === '*') { 50 | pattern = /\.[a-z]+$/i; // Anything with a extension 51 | } 52 | 53 | if (pattern) { 54 | patterns.push({ 55 | pattern: pattern, 56 | index: index 57 | }); 58 | matchInsertionCount['insert-'+index] = 0; 59 | } 60 | }); 61 | 62 | var fileFetchCallback = grouping(); 63 | fs.readdir(group.path, function(err, files) { 64 | if (err) { 65 | throw err; 66 | } 67 | files.forEach(function(fileName, index) { 68 | var alreadyIncluded = false, 69 | matchedPattern = false; 70 | 71 | group.files.forEach(function(includedFile) { 72 | if (alreadyIncluded || includedFile.trim && (includedFile.trim() === fileName.trim())) { 73 | alreadyIncluded = true; 74 | } 75 | }); 76 | 77 | if (!alreadyIncluded) { 78 | patterns.forEach(function(pattern) { 79 | if (!matchedPattern && pattern.pattern.exec(fileName)) { 80 | matchedPattern = pattern; 81 | } 82 | }); 83 | } 84 | if (matchedPattern) { 85 | insertions.push({ 86 | file: fileName, 87 | index: matchedPattern.index 88 | }); 89 | } 90 | }); 91 | insertions.forEach(function(insertion, index) { 92 | if (!matchInsertionCount['insert-'+insertion.index]) { 93 | group.files.splice(insertion.index, 1, insertion.file); 94 | } else { 95 | group.files.splice(insertion.index+matchInsertionCount['insert-'+insertion.index], 0, insertion.file); 96 | } 97 | matchInsertionCount['insert-'+insertion.index] += 1; 98 | }); 99 | fileFetchCallback(null, true); 100 | }); 101 | }); 102 | }, function(err, contents) { 103 | settings.forEach(function (group, groupName) { 104 | if (!group.stale) { 105 | group.files.forEach(function (file, index) { 106 | if (!file.match) { 107 | console.log('No match for: '+file); 108 | group.files.splice(index, 1); 109 | return; 110 | } 111 | if (file.match(/^https?:\/\//)) { 112 | return; 113 | } 114 | fs.watch(group.path + file, function (event, file) { 115 | if (event === 'change') { 116 | self.generateCache(groupName); 117 | } 118 | }); 119 | }); 120 | } 121 | }); 122 | self.generateCache(); 123 | }); 124 | 125 | this.generateCache = function (generateGroup) { 126 | var self = this; 127 | settings.forEach(function (group, groupName) { 128 | var userAgentMatches = {}; 129 | if (group.preManipulate) { 130 | Object.keys(group.preManipulate).forEach(function(key) { 131 | userAgentMatches[key] = true; 132 | }); 133 | } 134 | if (group.postManipulate) { 135 | Object.keys(group.postManipulate).forEach(function(key) { 136 | userAgentMatches[key] = true; 137 | }); 138 | } 139 | if (!Object.keys(userAgentMatches).length) { 140 | userAgentMatches = ['^']; 141 | } else { 142 | userAgentMatches = Object.keys(userAgentMatches); 143 | } 144 | 145 | userAgentMatches.forEach(function(match) { 146 | var path = group.path; 147 | Step(function () { 148 | var grouping = this.group(); 149 | group.files.forEach(function (file) { 150 | if (!generateGroup || generateGroup && groupName === generateGroup) { 151 | self.getFile(file, path, groupName, grouping()); 152 | } 153 | }); 154 | }, function (err, contents) { 155 | if (err) { 156 | throw err; 157 | } 158 | var grouping = this.group(); 159 | var lastModified = null; 160 | 161 | for (var i = 0, l = contents.length; i < l; i++) { 162 | var file = contents[i]; 163 | if (typeof file == "string"){ 164 | continue; 165 | } 166 | if (typeof file.modified != "undefined"){ 167 | file.modified = new Date(); 168 | } 169 | if (Object.prototype.toString.call(file.modified) === "[object Date]" && !isNaN(file.modified)){ 170 | 171 | } else { 172 | file.modified = new Date(); 173 | } 174 | if (!lastModified || lastModified.getTime() < file.modified.getTime()) { 175 | lastModified = file.modified; 176 | } 177 | if (!group.preManipulate) { 178 | group.preManipulate = {}; 179 | } 180 | 181 | self.manipulate(group.preManipulate[match], file.content, file.filePath, i, i === l - 1, grouping()); 182 | }; 183 | if (!lastModified && !contents.length) { 184 | grouping(); 185 | return; 186 | } 187 | cacheTimestamps[groupName] = lastModified.getTime(); 188 | if (!cache[groupName]) { 189 | cache[groupName] = {}; 190 | } 191 | cache[groupName][match] = { 192 | 'modified': lastModified.toUTCString() 193 | }; 194 | }, function (err, contents) { 195 | if (err) { 196 | throw err; 197 | } 198 | var grouping = this.group(); 199 | 200 | var content = ''; 201 | for (var i=0; i < contents.length; i++) { 202 | content += contents[i] + "\n"; 203 | }; 204 | var dataTypeLowerCase = group.dataType.toLowerCase(); 205 | if (!group.debug) { 206 | if (dataTypeLowerCase === 'javascript' || dataTypeLowerCase === 'js') { 207 | (function (callback){callback(null, jsmin(content));})(grouping()); 208 | } else if (dataTypeLowerCase === 'html') { 209 | (function (callback){callback(null, htmlmin(content));})(grouping()); 210 | } else if (dataTypeLowerCase === 'css') { 211 | (function (callback){callback(null, cssmin(content));})(grouping()); 212 | } 213 | } else { 214 | grouping()(null, content); 215 | } 216 | }, function (err, contents) { 217 | if (err) { 218 | throw err; 219 | } 220 | 221 | var grouping = this.group(); 222 | 223 | var content = ''; 224 | for (var i=0; i < contents.length; i++) { 225 | content += contents[i]; 226 | }; 227 | 228 | if (!group.postManipulate) { 229 | group.postManipulate = {}; 230 | } 231 | self.manipulate(group.postManipulate[match], content, null, 0, true, grouping()); 232 | 233 | }, function (err, contents) { 234 | if (err) { 235 | throw err; 236 | } 237 | 238 | var content = ''; 239 | for (var i=0; i < contents.length; i++) { 240 | content += contents[i]; 241 | }; 242 | 243 | cacheHashes[groupName] = crypto.createHash('md5').update(content).digest('hex'); 244 | 245 | cache[groupName][match].encodings = {}; 246 | var encodings = cache[groupName][match].encodings; 247 | 248 | var utf8Buffer = new Buffer(content, 'utf8'); 249 | encodings.utf8 = { 250 | 'buffer': utf8Buffer, 251 | 'length': utf8Buffer.length, 252 | 'encoding': false 253 | }; 254 | 255 | if(zlib) { 256 | var gzipBuffer = zlib.gzip(utf8Buffer, function(error, result) { 257 | encodings.gzip = { 258 | 'buffer': result, 259 | 'length': result.length, 260 | 'encoding': 'gzip' 261 | }; 262 | }); 263 | } 264 | }); 265 | }); 266 | }); 267 | }; 268 | 269 | this.manipulate = function (manipulateInstructions, fileContent, path, index, last, callback) { 270 | if (manipulateInstructions && Array.isArray(manipulateInstructions)) { 271 | var callIndex = 0; 272 | (function modify(content, path, index, last) { 273 | if (callIndex < manipulateInstructions.length) { 274 | callIndex++; 275 | manipulateInstructions[callIndex-1](content, path, index, last, function (content) { 276 | modify(content, path, index, last); 277 | }); 278 | } else { 279 | callback(null, content); 280 | } 281 | })(fileContent, path, index, last); 282 | } else if (manipulateInstructions && typeof manipulateInstructions === 'function') { 283 | manipulateInstructions(fileContent, path, index, last, callback); 284 | } else { 285 | callback(null, fileContent); 286 | } 287 | }; 288 | 289 | this.getFile = function (file, path, groupName, callback) { 290 | var isExternal = false; 291 | if (file && file.match(/^https?:\/\//)) { 292 | isExternal = true; 293 | } 294 | 295 | var fileInfo = { 296 | 'filePath': isExternal ? file: path+file 297 | }; 298 | 299 | if (isExternal) { 300 | request({uri: file}, function(err, res, body) { 301 | fileInfo.content = body; 302 | fileInfo.external = true; 303 | if (typeof res != "undefined" && res != null){ 304 | fileInfo.modified = new Date(res.headers['last-modified']); 305 | } 306 | callback(null, fileInfo); 307 | }); 308 | } else { 309 | setTimeout(function() { 310 | fs.readFile(path+file, function (err, data) { 311 | if (err) { 312 | console.log('Could not find: '+file); 313 | callback(null, ''); 314 | return; 315 | } 316 | fileInfo.content = data.toString(); 317 | 318 | fs.stat(path+file, function (err, stat) { 319 | fileInfo.modified = stat.mtime; 320 | callback(null, fileInfo); 321 | }); 322 | }); 323 | }, 100); 324 | } 325 | }; 326 | 327 | this.acceptsGzip = function(req) { 328 | var accept = req.headers["accept-encoding"]; 329 | return accept && accept.toLowerCase().indexOf('gzip') !== -1; 330 | } 331 | 332 | function assetManager (req, res, next) { 333 | var self = this; 334 | var found = false; 335 | var response = {}; 336 | var mimeType = 'text/plain'; 337 | var groupServed; 338 | settings.forEach(function (group, groupName) { 339 | if (group.route.test(req.url)) { 340 | var userAgent = req.headers['user-agent'] || ''; 341 | groupServed = group; 342 | if (group.dataType === 'javascript') { 343 | mimeType = 'application/javascript'; 344 | } 345 | else if (group.dataType === 'html') { 346 | mimeType = 'text/html'; 347 | } 348 | else if (group.dataType === 'css') { 349 | mimeType = 'text/css'; 350 | } 351 | if (cache[groupName]) { 352 | Object.keys(cache[groupName]).forEach(function(match) { 353 | if (!found && userAgent.match(new RegExp(match, 'i'))) { 354 | found = true; 355 | var item = cache[groupName][match]; 356 | 357 | var content = item.encodings.utf8; 358 | if(zlib && item.encodings.gzip && this.acceptsGzip(req)) { 359 | content = item.encodings.gzip; 360 | } 361 | 362 | response = { 363 | contentLength: content.length 364 | , modified: item.modified 365 | , contentBuffer: content.buffer 366 | , encoding: content.encoding 367 | }; 368 | } 369 | }); 370 | } 371 | } 372 | }); 373 | 374 | if (!found) { 375 | next(); 376 | } else { 377 | if (groupServed.serveModify) { 378 | groupServed.serveModify(req, res, response, function(response) { 379 | serveContent(response); 380 | }); 381 | } else { 382 | serveContent(response); 383 | } 384 | function serveContent(response) { 385 | var headers = { 386 | 'Last-Modified': response.modified, 387 | 'Date': (new Date).toUTCString(), 388 | 'Cache-Control': 'public,max-age=' + 31536000, 389 | 'Expires': response.expires || (new Date(new Date().getTime()+63113852000)).toUTCString(), 390 | 'Vary': 'Accept-Encoding' 391 | }; 392 | 393 | if (req.headers['if-modified-since'] && 394 | Date.parse(req.headers['if-modified-since']) >= Date.parse(response.modified)) { 395 | res.writeHead(304, headers); 396 | res.end(); 397 | } else { 398 | headers['Content-Type'] = mimeType; 399 | headers['Content-Length'] = response.contentLength; 400 | 401 | if(response.encoding) { 402 | headers['Content-Encoding'] = response.encoding 403 | } 404 | 405 | res.writeHead(200, headers); 406 | res.end(response.contentBuffer); 407 | } 408 | } 409 | return; 410 | } 411 | }; 412 | 413 | assetManager.cacheTimestamps = cacheTimestamps; 414 | assetManager.cacheHashes = cacheHashes; 415 | 416 | return assetManager; 417 | }; 418 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "connect-assetmanager", 3 | "description" : "Middleware for Connect (node.js) for handling your static assets.", 4 | "version" : "0.0.28", 5 | "author" : "Mathias Pettersson ", 6 | "keywords": ["build", "assets", "css", "javascript"], 7 | "engines" : ["node"], 8 | "directories" : { "lib" : "./lib" }, 9 | "main" : "./lib/assetmanager", 10 | "repository" : [ 11 | { "type":"git", "url":"http://github.com/mape/connect-assetmanager.git" } 12 | ], 13 | "dependencies" : { 14 | "request" : ">=0.10.0", 15 | "step" : ">=0.0.3" 16 | } 17 | } 18 | --------------------------------------------------------------------------------