├── 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 |
--------------------------------------------------------------------------------