├── .gitignore ├── test ├── fixtures │ ├── print.css.gz │ ├── print.css │ └── style.css ├── mocha.opts ├── gzip-static.js └── support │ └── http.js ├── .travis.yml ├── index.js ├── .npmignore ├── Makefile ├── History.md ├── .jshintrc ├── package.json ├── License ├── Readme.md └── lib └── gzip-static.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /test/fixtures/print.css.gz: -------------------------------------------------------------------------------- 1 | compressed -------------------------------------------------------------------------------- /test/fixtures/print.css: -------------------------------------------------------------------------------- 1 | body{color:"green";} -------------------------------------------------------------------------------- /test/fixtures/style.css: -------------------------------------------------------------------------------- 1 | body{color:"red";} -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/gzip-static'); -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --require test/support/http 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | Makefile 3 | History.md 4 | License 5 | .travis.yml 6 | .jshintrc 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: lint test 2 | 3 | lint: 4 | ./node_modules/.bin/jshint *.js lib test 5 | 6 | test: 7 | ./node_modules/.bin/mocha --recursive 8 | 9 | .PHONY: all lint test 10 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.1.1 / 2013-09-17 3 | ================== 4 | 5 | * Only handle GET and HEAD methods 6 | 7 | 0.1.0 / 2013-09-17 8 | ================== 9 | 10 | * Initial implementation of gzip-static middleware 11 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true, 3 | "unused": true, 4 | "laxbreak": true, 5 | "laxcomma": true, 6 | "proto": true, 7 | "globals": { 8 | "console": false, 9 | "exports": false, 10 | "process": false, 11 | "require": false, 12 | "module": false, 13 | "__dirname": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-gzip-static", 3 | "version": "0.1.1", 4 | "description": "gzip static middleware for connect - serves compressed files if they exist, falls through to connect-static if they don't", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make lint test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:code42day/connect-gzip-static.git" 12 | }, 13 | "keywords": [ 14 | "connect", 15 | "middleware", 16 | "gzip", 17 | "compress" 18 | ], 19 | "author": "Damian Krzeminski ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/code42day/connect-gzip-static/issues" 23 | }, 24 | "dependencies": { 25 | "debug": "0.7.4", 26 | "connect": "2.12.0", 27 | "mime": "1.2.11" 28 | }, 29 | "devDependencies": { 30 | "jshint": "2.4.2", 31 | "mocha": "1.17.1", 32 | "should": "3.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 code42day 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/code42day/connect-gzip-static.png)](http://travis-ci.org/code42day/connect-gzip-static) 2 | [![Dependency Status](https://gemnasium.com/code42day/connect-gzip-static.png)](https://gemnasium.com/code42day/connect-gzip-static) 3 | [![NPM version](https://badge.fury.io/js/connect-gzip-static.png)](http://badge.fury.io/js/connect-gzip-static) 4 | 5 | # connect-gzip-static 6 | 7 | Middleware for [connect][]: serves compressed files if they exist, falls through to connect-static 8 | if they don't, or if browser does not send 'Accept-Encoding' header. 9 | 10 | You should use `connect-gzip-static` if your build process already creates gzipped files. If you 11 | want to gzip your data on the fly use built-in [connect compress][] middleware. And if you want to 12 | gzip your files dynamically you may want to look up [connect gzip][]. 13 | 14 | ## Installation 15 | 16 | $ npm install connect-gzip-static 17 | 18 | ## Options 19 | 20 | gzip-static is meant to be a drop in replacement for [connect static][] middleware. Use the same 21 | options as you would with [connect static][]. 22 | 23 | 24 | ## Usage 25 | 26 | ```javascript 27 | var gzipStatic = require('connect-gzip-static'); 28 | var oneDay = 86400000; 29 | 30 | connect() 31 | .use(gzipStatic(__dirname + '/public')) 32 | 33 | connect() 34 | .use(gzipStatic(__dirname + '/public', { maxAge: oneDay })) 35 | ``` 36 | 37 | # License 38 | 39 | MIT 40 | 41 | [connect]: http://www.senchalabs.org/connect 42 | [connect static]: http://www.senchalabs.org/connect/static.html 43 | [connect compress]: http://www.senchalabs.org/connect/compress.html 44 | [connect gzip]: https://github.com/tikonen/connect-gzip 45 | -------------------------------------------------------------------------------- /lib/gzip-static.js: -------------------------------------------------------------------------------- 1 | var mime = require('mime'); 2 | var connect = require('connect'); 3 | var debug = require('debug')('connect:gzip-static'); 4 | var utils = connect.utils; 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | /*var debug = console.log.bind(console);*/ 9 | 10 | function setHeaders(res, name) { 11 | debug('content-type %s', name.mime_type); 12 | res.setHeader('Content-Type', name.mime_type + (name.charset ? '; charset=' + name.charset : '')); 13 | res.setHeader('Content-Encoding', 'gzip'); 14 | res.setHeader('Vary', 'Accept-Encoding'); 15 | } 16 | 17 | var names = {}; 18 | 19 | module.exports = function(root, options) { 20 | var static; 21 | 22 | options = options || {}; 23 | static = connect.static(root, options); 24 | 25 | return function gzipStatic(req, res, next) { 26 | if ('GET' !== req.method && 'HEAD' !== req.method) { 27 | return next(); 28 | } 29 | 30 | var acceptEncoding = req.headers['accept-encoding'] || ''; 31 | if (! options.force && acceptEncoding.indexOf('gzip') === -1) { 32 | return static.call(this, req, res, next); 33 | } 34 | 35 | var name = names[req.url]; 36 | if ( ! name) { 37 | name = {}; 38 | name.orig = utils.parseUrl(req).pathname; 39 | name.gz = name.orig + '.gz'; 40 | name.full = path.join(root, name.gz); 41 | name.mime_type = mime.lookup(name.orig); 42 | name.charset = mime.charsets.lookup(name.mime_type); 43 | 44 | names[req.url] = name; 45 | } 46 | 47 | fs.stat(name.full, function(err, stat) { 48 | var exists = !err && stat.isFile(); 49 | if (exists) { 50 | req.url = name.gz; 51 | setHeaders(res, name); 52 | } 53 | debug('Sending %s', req.url); 54 | static.call(this, req, res, next); 55 | }); 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /test/gzip-static.js: -------------------------------------------------------------------------------- 1 | var gzipStatic = require('..'); 2 | 3 | var connect = require('connect'); 4 | var fixtures = __dirname + '/fixtures'; 5 | var app = connect(); 6 | 7 | /* global describe, it */ 8 | 9 | 10 | app.use(gzipStatic(fixtures)); 11 | 12 | app.use(function(req, res){ 13 | res.statusCode = 404; 14 | res.end('sorry!'); 15 | }); 16 | 17 | describe('gzipStatic', function(){ 18 | it('should serve static files', function(done){ 19 | app.request() 20 | .get('/style.css') 21 | .expect('body{color:"red";}', done); 22 | }); 23 | 24 | it('should set Content-Type', function(done){ 25 | app.request() 26 | .get('/style.css') 27 | .expect('Content-Type', 'text/css; charset=UTF-8', done); 28 | }); 29 | 30 | it('should default max-age=0', function(done){ 31 | app.request() 32 | .get('/style.css') 33 | .expect('Cache-Control', 'public, max-age=0', done); 34 | }); 35 | 36 | it('should serve uncompressed files unless requested', function(done){ 37 | app.request() 38 | .get('/print.css') 39 | .expect('body{color:"green";}', done); 40 | }); 41 | 42 | it('should serve compressed files when requested', function(done){ 43 | app.request() 44 | .set('Accept-Encoding', 'gzip') 45 | .get('/print.css') 46 | .expect('compressed', done); 47 | }); 48 | 49 | it('should set Content-Type for compressed files', function(done){ 50 | app.request() 51 | .set('Accept-Encoding', 'gzip') 52 | .get('/print.css') 53 | .expect('Content-Type', 'text/css; charset=UTF-8', done); 54 | }); 55 | 56 | it('should set Vary for compressed files', function(done){ 57 | app.request() 58 | .set('Accept-Encoding', 'gzip') 59 | .get('/print.css') 60 | .expect('Vary', 'Accept-Encoding', done); 61 | }); 62 | 63 | it('should set Content-Encoding for compressed files', function(done){ 64 | app.request() 65 | .set('Accept-Encoding', 'gzip') 66 | .get('/print.css') 67 | .expect('Content-Encoding', 'gzip', done); 68 | }); 69 | 70 | it('should ignore POST requests', function(done){ 71 | app.request() 72 | .set('Accept-Encoding', 'gzip') 73 | .post('/print.css') 74 | .expect(404, done); 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /test/support/http.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var EventEmitter = require('events').EventEmitter 7 | , methods = ['get', 'post', 'put', 'delete', 'head'] 8 | , connect = require('connect') 9 | , http = require('http'); 10 | 11 | module.exports = request; 12 | 13 | connect.proto.request = function(){ 14 | return request(this); 15 | }; 16 | 17 | function request(app) { 18 | return new Request(app); 19 | } 20 | 21 | function Request(app) { 22 | var self = this; 23 | this.data = []; 24 | this.header = {}; 25 | this.app = app; 26 | if (!this.server) { 27 | this.server = http.Server(app); 28 | this.server.listen(0, function(){ 29 | self.addr = self.server.address(); 30 | self.listening = true; 31 | }); 32 | } 33 | } 34 | 35 | /** 36 | * Inherit from `EventEmitter.prototype`. 37 | */ 38 | 39 | Request.prototype.__proto__ = EventEmitter.prototype; 40 | 41 | methods.forEach(function(method){ 42 | Request.prototype[method] = function(path){ 43 | return this.request(method, path); 44 | }; 45 | }); 46 | 47 | Request.prototype.set = function(field, val){ 48 | this.header[field] = val; 49 | return this; 50 | }; 51 | 52 | Request.prototype.write = function(data){ 53 | this.data.push(data); 54 | return this; 55 | }; 56 | 57 | Request.prototype.request = function(method, path){ 58 | this.method = method; 59 | this.path = path; 60 | return this; 61 | }; 62 | 63 | Request.prototype.expect = function(body, fn){ 64 | var args = arguments; 65 | this.end(function(res){ 66 | switch (args.length) { 67 | case 3: 68 | res.headers.should.have.property(body.toLowerCase(), args[1]); 69 | args[2](); 70 | break; 71 | default: 72 | if ('number' == typeof body) { 73 | res.statusCode.should.equal(body); 74 | } else { 75 | res.body.should.equal(body); 76 | } 77 | fn(); 78 | } 79 | }); 80 | }; 81 | 82 | Request.prototype.end = function(fn){ 83 | var self = this; 84 | 85 | if (this.listening) { 86 | var req = http.request({ 87 | method: this.method 88 | , port: this.addr.port 89 | , host: this.addr.address 90 | , path: this.path 91 | , headers: this.header 92 | }); 93 | 94 | this.data.forEach(function(chunk){ 95 | req.write(chunk); 96 | }); 97 | 98 | req.on('response', function(res){ 99 | var buf = ''; 100 | res.setEncoding('utf8'); 101 | res.on('data', function(chunk){ buf += chunk; }); 102 | res.on('end', function(){ 103 | res.body = buf; 104 | fn(res); 105 | }); 106 | }); 107 | 108 | req.end(); 109 | } else { 110 | this.server.on('listening', function(){ 111 | self.end(fn); 112 | }); 113 | } 114 | 115 | return this; 116 | }; 117 | --------------------------------------------------------------------------------