Comparing version 0.0.3 to 0.0.4
@@ -0,1 +1,6 @@ | ||
## v0.0.4 | ||
* Add support to use `jac.resolve` with CSS files (offline) | ||
* Allow json and js files to be used as configuration inputs for ./bin/jac | ||
## v0.0.3 | ||
@@ -2,0 +7,0 @@ |
@@ -0,4 +1,7 @@ | ||
"use strict"; | ||
module.exports = { | ||
create: require('./src/middleware').create, | ||
digest: require('./src/digester').process | ||
middleware: require('./src/middleware').create, | ||
digest: require('./src/digester').process, | ||
css: require('./src/css').process | ||
}; |
{ | ||
"name": "jac", | ||
"version": "0.0.3", | ||
"version": "0.0.4", | ||
"description": "jac provides methods to reference asset urls using permanently cachable urls.", | ||
@@ -17,6 +17,7 @@ "main": "index", | ||
"express": "~2.5.11", | ||
"should": "~1.2.1" | ||
"should": "~1.2.1", | ||
"URIjs": "~1.8.3" | ||
}, | ||
"scripts": { | ||
"test": "./node_modules/.bin/mocha --reporter spec --bail" | ||
"test": "./node_modules/.bin/mocha --reporter spec" | ||
}, | ||
@@ -36,2 +37,2 @@ "dependencies": { | ||
] | ||
} | ||
} |
@@ -1,2 +0,14 @@ | ||
#jac | ||
#jac [![Build Status](https://travis-ci.org/busbud/jac.png)](https://travis-ci.org/busbud/jac) | ||
> Really when you push something out live to the world, you never want to change it without changing the name because | ||
> there are so many misconfigured proxies out there. About 1% to 10% of your users will never get an update | ||
> unless you change the name. | ||
> You can make it cachable for 10 years, you're never going to push a change without changing the name of the file.<br/> | ||
> <cite>- Steve Souders. HTML5DevConf (Jan 10, 2013). | ||
<a href="http://marakana.com/s/post/1360/cache_is_king_steve_souders_html5_video">Cache is king</a> | ||
availalable from <a href="http://mrkn.co/3wzua">http://mrkn.co/3wzua</a></cite> | ||
jac provides methods to reference asset urls using permanently cachable urls. | ||
@@ -7,4 +19,2 @@ | ||
[![Build Status](https://travis-ci.org/busbud/jac.png)](https://travis-ci.org/busbud/jac) | ||
#Usage | ||
@@ -26,8 +36,8 @@ jac middleware will handle asset file serving and asset url resolution | ||
var express = require('express') | ||
, config = require('./config') | ||
, jac = require('jac').create(config) | ||
, app = express.createServer(); | ||
, jac = require('jac') | ||
, app = express.createServer() | ||
, config = require('./config'); | ||
// Add middleware for all requests | ||
app.use(jac.middleware); | ||
app.use(jac.middleware(config)); | ||
@@ -37,3 +47,3 @@ // Add view that resolves an image url | ||
var jac = res.local('jac') // returns jac view helper | ||
, key = '/images/spacer.gif' // matches config key | ||
, key = '/images/happy.png' // matches config key | ||
, url = jac.resolve(key); // returns url with digest, handled by middleware | ||
@@ -53,5 +63,6 @@ | ||
assets: [{ | ||
fullPath: require('path').resolve(__dirname, './public/images/spacer.gif'), | ||
key: '/images/spacer.gif', | ||
route: '/images/spacer.gif?b64Digest' | ||
fullPath: require('path').resolve(__dirname, './public/images/happy.png'), // local file path | ||
key: '/images/happy.png', // key used in views: jac.resolve('/images/happy.png') | ||
route: '/images/b64Digest/happy.png', // route for middleware | ||
url: '//cdn.host.net/images/b64Digest/happy.png' // url output to response and css | ||
}], | ||
@@ -89,6 +100,52 @@ | ||
### Replacing references in CSS | ||
jac can be configured to process CSS files as part of the update process. It will ignore image references from external | ||
sites (eg urls with an [authority](http://medialize.github.com/URI.js/docs.html#accessors-authority)) and data-uris, | ||
and will attempt to resolve all other urls. | ||
If it fails to resolve a url, it will throw an error, allowing you to find the problematic reference. Most likely, this | ||
will easily be corrected by adjusting the path to the image to make it root relative. | ||
Here's an example that will result in jac replacing the image reference | ||
__src/stylesheets/main.css__ | ||
```css | ||
body {background: url(/images/happy.png);} | ||
``` | ||
__config.json__ | ||
```js | ||
{ | ||
// required - files served by jac | ||
assets: [{ | ||
fullPath: require('path').resolve(__dirname, './public/images/happy.png'), // local file path | ||
key: '/images/happy.png', // key used in views: jac.resolve('/images/happy.png') | ||
route: '/images/b64Digest/happy.png', // route for middleware | ||
url: '//cdn.host.net/images/b64Digest/happy.png' // url output to response and css | ||
}], | ||
css: { | ||
'public/stylesheets/main.css': 'src/stylesheets/main.css' // format <output>: <input> | ||
} | ||
} | ||
``` | ||
To run the CSS replacement, update the jac config file to include the css property and use the following command | ||
```bash | ||
css --config ./config.json | ||
``` | ||
The file at public/stylesheets/main.css will now contain the following | ||
```css | ||
body {background: url(//cdn.host.net/images/b64Digest/happy.png);} | ||
``` | ||
## Compatibility | ||
This version of jac is compatible with express 2.5. | ||
It depends on `res.local()` to get and set the view local `jac.img` via middleware. | ||
It depends on `res.local()` to get and set the view local `jac` via middleware. | ||
@@ -119,2 +176,4 @@ ## Production | ||
By default, jac will load the config from the `jac.json` file at the project root. | ||
# Running Tests | ||
@@ -121,0 +180,0 @@ To run the test suite first invoke the following command within the repo, installing the development dependencies: |
@@ -0,1 +1,3 @@ | ||
"use strict"; | ||
var _ = require('lodash'); | ||
@@ -5,2 +7,3 @@ var url = require('url'); | ||
var async = require('async'); | ||
var URIjs = require('URIjs'); | ||
var readdirp = require('readdirp'); | ||
@@ -15,3 +18,3 @@ | ||
if (!_.isFunction(strategy)) { | ||
strategy = require('./strategy/hash').create(); | ||
strategy = require('./strategy/hash').create(opts); | ||
digestfn = strategy.digest.bind(strategy); | ||
@@ -23,6 +26,2 @@ } | ||
function routeName(entry) { | ||
return entry.url + '?' + entry.digest; | ||
} | ||
/** | ||
@@ -38,2 +37,3 @@ * Processes all files under opts.root and generates the config | ||
var vdir = opts.vdir || '/'; | ||
var host = opts.host || ''; | ||
var filter = opts.fileFilter || ['*.gif', '*.jpg', '*.jpeg', '*.png']; | ||
@@ -76,4 +76,4 @@ var silent = opts.silent; | ||
fullPath: path.relative(base, f.fullPath), | ||
key: f.path, | ||
url: url.resolve(vdir, f.path) | ||
key: url.resolve(vdir, f.path), | ||
url: (new URIjs(url.resolve(vdir, f.path))).host(host).href() | ||
}; | ||
@@ -100,3 +100,11 @@ } | ||
.map(function (entry) { | ||
entry.route = routeName(entry); | ||
// generate the urls and route by injecting the digest into the path | ||
var original = new URIjs(entry.url); | ||
var modified = original | ||
.clone() | ||
.directory(original.directory() + '/' + entry.digest); | ||
entry.url = modified.href(); | ||
entry.route = modified.host('').href(); | ||
return entry; | ||
@@ -103,0 +111,0 @@ }) |
@@ -0,1 +1,3 @@ | ||
"use strict"; | ||
var _ = require('lodash'); | ||
@@ -13,3 +15,3 @@ var send = require('send'); | ||
var routes = assets.reduce(function (memo, entry) { | ||
var assetsByRoute = assets.reduce(function (memo, entry) { | ||
memo[entry.route] = entry; | ||
@@ -19,4 +21,4 @@ return memo; | ||
var keys = assets.reduce(function (memo, entry) { | ||
memo[entry.key] = entry.route; | ||
var assetsByKey = assets.reduce(function (memo, entry) { | ||
memo[entry.key] = entry; | ||
return memo; | ||
@@ -35,10 +37,10 @@ }, {}); | ||
*/ | ||
function resolve (key) { | ||
var route = keys[key]; | ||
function resolveAsset (key) { | ||
var asset = assetsByKey[key]; | ||
if (!route) { | ||
throw new Error('jac-img: key ' + key + ' not found, regenerate jac-img config'); | ||
if (!asset) { | ||
throw new Error('jac: key ' + key + ' not found, regenerate jac config or update key value to match key in jac config'); | ||
} | ||
return route; | ||
return asset.url; | ||
} | ||
@@ -53,3 +55,3 @@ | ||
var jac = res.local('jac') || {}; | ||
jac.resolve = resolve; | ||
jac.resolve = resolveAsset; | ||
@@ -60,5 +62,5 @@ res.local('jac', jac); | ||
function middleware (req, res, next) { | ||
var hit = routes[req.url]; | ||
var asset = assetsByRoute[req.url]; | ||
if (!hit) { | ||
if (!asset) { | ||
locals(res); | ||
@@ -73,3 +75,3 @@ return next(); | ||
send(req, hit.fullPath) | ||
send(req, asset.fullPath) | ||
.maxage(config.maxAge) | ||
@@ -80,6 +82,3 @@ .on('error', next) | ||
return { | ||
middleware: middleware, | ||
resolve: resolve | ||
}; | ||
return middleware; | ||
}; |
@@ -0,1 +1,3 @@ | ||
"use strict"; | ||
var fs = require('fs'); | ||
@@ -5,8 +7,9 @@ var crypto = require('crypto'); | ||
module.exports.create = function (opts) { | ||
return new hashStrategy(opts); | ||
return new HashStrategy(opts); | ||
}; | ||
function hashStrategy(opts) { | ||
function HashStrategy(opts) { | ||
this.algorithm = opts && opts.algorithm || 'md5'; | ||
this.length = opts && opts.length || 7; | ||
this.salt = opts && opts.salt || ''; | ||
this.length = opts && opts.length || 7; | ||
} | ||
@@ -20,3 +23,3 @@ | ||
*/ | ||
hashStrategy.prototype.digest = function (entry, done) { | ||
HashStrategy.prototype.digest = function (entry, done) { | ||
var self = this; | ||
@@ -27,2 +30,4 @@ var hash = crypto.createHash(self.algorithm); | ||
hash.update(self.salt.toString()); | ||
s.on('data', function(d) { | ||
@@ -29,0 +34,0 @@ hash.update(d); |
@@ -0,1 +1,3 @@ | ||
"use strict"; | ||
var should = require('should'); | ||
@@ -64,10 +66,81 @@ var path = require('path'); | ||
it('entries\' route should have a querystring digest', function () { | ||
/** | ||
Don't include a query string in the URL for static resources. | ||
Most proxies, most notably Squid up through version 3.0, do not cache resources with a "?" in | ||
their URL even if a Cache-control: public header is present in the response. To enable proxy | ||
caching for these resources, remove query strings from references to static resources, and | ||
instead encode the parameters into the file names themselves. | ||
- https://developers.google.com/speed/docs/best-practices/caching | ||
*/ | ||
it('entries\' route should not have a querystring, but should contain digest in path', function () { | ||
entries.forEach(verify); | ||
function verify (e) { | ||
e.route.should.equal(e.url + '?' + e.digest); | ||
e.route.indexOf('?').should.equal(-1); | ||
e.route.indexOf(e.digest).should.not.equal(-1); | ||
e.url.indexOf('?').should.equal(-1); | ||
e.url.indexOf(e.digest).should.not.equal(-1); | ||
} | ||
}); | ||
it('entries\' key should start with a \'/\'', function () { | ||
entries.forEach(verify); | ||
function verify (e) { | ||
e.key[0].should.equal('/'); | ||
} | ||
}); | ||
}); | ||
describe('digest directory with explicit host', function () { | ||
var entries, error; | ||
before(function (done) { | ||
var opts = { | ||
root: __dirname, | ||
fileFilter: ["*.gif"], | ||
silent: false, | ||
host: 'cdn.net' | ||
}; | ||
digester.process(opts, cb); | ||
function cb (err, res) { | ||
error = err; | ||
entries = res; | ||
done(); | ||
} | ||
}); | ||
it('entries should have the host set only on the url', function () { | ||
entries.forEach(verify); | ||
function verify (e) { | ||
e.should.have.property('fullPath'); | ||
e.should.have.property('key'); | ||
e.should.have.property('url'); | ||
e.should.have.property('digest'); | ||
e.should.have.property('route'); | ||
var prefix = '//cdn.net/'; | ||
e.url.indexOf(prefix).should.equal(0); | ||
e.route.indexOf(prefix).should.equal(-1); | ||
e.key.indexOf(prefix).should.equal(-1); | ||
} | ||
}); | ||
it('entries\' route should not have a querystring, but should contain digest in path', function () { | ||
entries.forEach(verify); | ||
function verify (e) { | ||
e.route.indexOf('?').should.equal(-1); | ||
e.route.indexOf(e.digest).should.not.equal(-1); | ||
e.url.indexOf('?').should.equal(-1); | ||
e.url.indexOf(e.digest).should.not.equal(-1); | ||
} | ||
}); | ||
}); | ||
}); |
@@ -0,1 +1,3 @@ | ||
"use strict"; | ||
var should = require('should'); | ||
@@ -20,8 +22,8 @@ var path = require('path'); | ||
fullPath: path.resolve(__dirname, './fixtures/spacer.gif'), | ||
key: 'images/spacer.gif', | ||
route: '/images/spacer.gif.b64Digest', | ||
mtime: new Date() | ||
key: '/images/spacer.gif', | ||
route: '/images/b64Digest/spacer.gif', | ||
url: '/images/b64Digest/spacer.gif' | ||
} | ||
] | ||
}).middleware; | ||
}); | ||
@@ -34,6 +36,6 @@ app = express.createServer(); | ||
app.get('/view-spacer', function (req, res) { | ||
res.send(res.local('jac').resolve('images/spacer.gif')); | ||
res.send(res.local('jac').resolve('/images/spacer.gif')); | ||
}); | ||
app.get('/view-unresolved', function (req, res) { | ||
res.send(res.local('jac').resolve('images/noexisto.gif')); | ||
res.send(res.local('jac').resolve('/images/noexisto.gif')); | ||
}); | ||
@@ -59,3 +61,3 @@ app.error(function (err, req, res, next) { | ||
request(app) | ||
.get('/images/spacer.gif.b64Digest') | ||
.get('/images/b64Digest/spacer.gif') | ||
.expect('Cache-Control', 'public, max-age=' + twoweeks) | ||
@@ -69,3 +71,3 @@ .expect('Content-Type', 'image/gif') | ||
request(app) | ||
.get('/images/spacer.gif.b64DigestX') | ||
.get('/images/b64DigestX/spacer.gif') | ||
.expect(404, done); | ||
@@ -83,3 +85,3 @@ }); | ||
.get('/view-spacer') | ||
.expect(200, '/images/spacer.gif.b64Digest', done); | ||
.expect(200, '/images/b64Digest/spacer.gif', done); | ||
}); | ||
@@ -90,5 +92,5 @@ | ||
.get('/view-unresolved') | ||
.expect(500, 'jac-img: key images/noexisto.gif not found, regenerate jac-img config', done); | ||
.expect(500, 'jac: key /images/noexisto.gif not found, regenerate jac config or update key value to match key in jac config', done); | ||
}); | ||
}); | ||
}); |
@@ -0,1 +1,3 @@ | ||
"use strict"; | ||
var should = require('should'); | ||
@@ -11,67 +13,75 @@ var path = require('path'); | ||
// Setup algorithm/hash mapping for spacer file | ||
var algorithms = { | ||
'md5': 'MlRyYBVx8x4b8AZ0w2jTNQ==', | ||
'sha1':'La6qi18Z8LwgnZdsAr1qy1GwCwo=' | ||
var digests = { | ||
'': { | ||
'md5': 'MlRyYBVx8x4b8AZ0w2jTNQ==', | ||
'sha1':'La6qi18Z8LwgnZdsAr1qy1GwCwo=' | ||
}, | ||
1: { | ||
'md5': 'TKX3uHbkUGXNoc4yxKgLJA==', | ||
'sha1':'06ZZ1arUCeEiXsqE3_CAzeJN-LY=' | ||
} | ||
}; | ||
var lengths = [5, 7]; | ||
lengths.forEach(function (length) { | ||
Object.keys(algorithms).forEach(function(algo) { | ||
var expectedDigest = algorithms[algo].substr(0, length); | ||
Object.keys(digests).forEach(function (salt) { | ||
Object.keys(digests[salt]).forEach(function(algo) { | ||
var expectedDigest = digests[salt][algo].substr(0, length); | ||
describe(length + ' char ' + algo + ' digest', function () { | ||
var strategy = factory.create({ | ||
algorithm: algo, | ||
length: length | ||
}); | ||
describe(length + ' char ' + algo + ' digest with ' + (salt||'no') + ' salt', function () { | ||
var strategy = factory.create({ | ||
algorithm: algo, | ||
length: length, | ||
salt: salt | ||
}); | ||
it('should not be null', function () { | ||
should.exist(strategy); | ||
}); | ||
it('should not be null', function () { | ||
should.exist(strategy); | ||
}); | ||
it('should have a digest method', function () { | ||
should.exist(strategy.digest); | ||
strategy.digest.should.be.instanceof(Function); | ||
}); | ||
it('should have a digest method', function () { | ||
should.exist(strategy.digest); | ||
strategy.digest.should.be.instanceof(Function); | ||
}); | ||
describe('handles existing content', function () { | ||
var entry = { | ||
fullPath: path.resolve(__dirname, './fixtures/spacer.gif') | ||
}; | ||
describe('handles existing content', function () { | ||
var entry = { | ||
fullPath: path.resolve(__dirname, './fixtures/spacer.gif') | ||
}; | ||
before(function (done) { | ||
strategy.digest(entry, done); | ||
}); | ||
before(function (done) { | ||
strategy.digest(entry, done); | ||
}); | ||
it('should set digest as first N chars of hash', function () { | ||
should.exist(entry); | ||
entry.should.have.ownProperty('digest'); | ||
entry.digest.should.equal(expectedDigest); | ||
entry.digest.should.have.length(length); | ||
it('should set digest as first N chars of hash', function () { | ||
should.exist(entry); | ||
entry.should.have.ownProperty('digest'); | ||
entry.digest.should.equal(expectedDigest); | ||
entry.digest.should.have.length(length); | ||
}); | ||
}); | ||
}); | ||
describe('handles non-existent content', function () { | ||
var error; | ||
var entry = { | ||
fullPath: path.resolve(__dirname, '../fixtures/no existo.gif') | ||
}; | ||
describe('handles non-existent content', function () { | ||
var error; | ||
var entry = { | ||
fullPath: path.resolve(__dirname, '../fixtures/no existo.gif') | ||
}; | ||
before(function (done) { | ||
strategy.digest(entry, function (err, result) { | ||
error = err; | ||
entry = result; | ||
done(); | ||
before(function (done) { | ||
strategy.digest(entry, function (err, result) { | ||
error = err; | ||
entry = result; | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('should call back with error', function () { | ||
should.exist(error); | ||
error.code.should.equal('ENOENT'); | ||
}); | ||
it('should call back with error', function () { | ||
should.exist(error); | ||
error.code.should.equal('ENOENT'); | ||
}); | ||
it('entry should be undefined', function () { | ||
should.not.exist(entry); | ||
it('entry should be undefined', function () { | ||
should.not.exist(entry); | ||
}); | ||
}); | ||
@@ -78,0 +88,0 @@ }); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
37277
22
696
190
5
3