Comparing version 0.1.3 to 0.1.4
# Artist changes | ||
* 0.1.4 | ||
1. Added proxy() middleware | ||
* 0.1.3 | ||
@@ -4,0 +6,0 @@ 1. Option `refresh` is replaced by `cache` |
130
index.js
@@ -109,2 +109,130 @@ var defaults = { | ||
} | ||
} | ||
var formats = { | ||
'amd': 'define(function(){return #FN#});' | ||
}; | ||
/** | ||
* Returns a Connect's middleware. | ||
* | ||
* Allows to compile the templates into JavaScript files on the fly. | ||
* Supports JSONP, AMD or custom output formatting. | ||
* | ||
* Options: | ||
* | ||
* - `route` mounting point for the middleware | ||
* - `templates` path to the templates directory | ||
* - `format` output formating string or 'amd' (default) or 'jsonp' | ||
* - `maxAge` http cache max-age in milliseconds (defaults to 0) | ||
* - `cache` cache compiled functions | ||
* - `beautify` beautify compiled functions | ||
* - `debug` include debugging information | ||
* | ||
* @param {Options} options | ||
* @return {Function} | ||
*/ | ||
exports.proxy = function (options) { | ||
var fs = require('fs'), | ||
url = require('url'), | ||
fest = require('fest'), | ||
cache = {}; | ||
options = extend({ | ||
maxAge: 0, | ||
format: 'amd' | ||
}, defaults, options); | ||
if (!options.route) throw new Error('route is required'); | ||
if (!options.templates) throw new Error('tempates is required'); | ||
if (options.route[0] !== '/') options.route = '/' + options.route; | ||
if (options.route.slice(-1) !== '/') options.route += '/'; | ||
if (options.format in formats) { | ||
options.format = formats[options.format]; | ||
} | ||
return function (req, res, next) { | ||
if (req.method !== 'HEAD' && req.method !== 'GET') return next(); | ||
try { | ||
var uo = url.parse(req.url, true), | ||
path = decodeURIComponent(uo.pathname), | ||
jsonp = decodeURIComponent(uo.query.jsonp); | ||
} catch (e) { | ||
return next(e); | ||
} | ||
if (0 !== path.indexOf(options.route) || path.slice(-3) !== '.js') { | ||
return next(); | ||
} | ||
var id = path.slice(options.route.length).slice(0, -3); | ||
path = options.templates + '/' + id + '.xml'; | ||
function send(file) { | ||
if (options.format === 'jsonp' && jsonp) { | ||
file.content = jsonp + '(' + file.content + ');'; | ||
file.ts = new Date; | ||
} | ||
res.setHeader('Date', (new Date).toUTCString()); | ||
res.setHeader('Cache-Control', 'public, max-age=' + options.maxAge); | ||
res.setHeader('Last-Modified', file.ts.toUTCString()); | ||
var ms = req.headers['if-modified-since']; | ||
if (ms) { | ||
ms = new Date(ms); | ||
if (!isNaN(ms.valueOf()) && ms >= file.ts) { | ||
res.statusCode = 304; | ||
return res.end(); | ||
} | ||
} | ||
res.setHeader('Content-Type', 'application/javascript'); | ||
res.setHeader('Content-Length', Buffer.byteLength(file.content, 'utf8')); | ||
if (req.method == 'HEAD') { | ||
res.end(); | ||
} else { | ||
res.end(file.content); | ||
} | ||
} | ||
if (options.cache && path in cache) { | ||
send(cache[path]); | ||
} else { | ||
fs.stat(path, function (err, stats) { | ||
if (err) { | ||
return next(); | ||
} else try { | ||
if (!stats.isFile()) { | ||
return next(); | ||
} | ||
var content = fest.compile(path, options); | ||
if (options.format !== 'jsonp') { | ||
content = options.format | ||
.replace(/#ID#/g, id.replace(/\'/g, "\\'").replace(/\"/g, '\\"').replace(/\\/g, '\\')) | ||
.replace(/#FN#/g, content); | ||
} | ||
var file = { | ||
content: content, | ||
ts: new Date | ||
}; | ||
if (options.cache) { | ||
cache[path] = file; | ||
} | ||
send(file); | ||
} catch (err) { | ||
next(err); | ||
} | ||
}); | ||
} | ||
}; | ||
} |
{ | ||
"name": "artist", | ||
"version": "0.1.3", | ||
"version": "0.1.4", | ||
"description": "Template engine for node.js built on fest", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -13,2 +13,3 @@ # Artist | ||
* Handles fest's runtime errors | ||
* Connect's middleware proxy | ||
@@ -23,3 +24,3 @@ ## Installation | ||
Provides `render()` and `renderSync()` functions. View source for documentation. | ||
View source for documentation. | ||
@@ -55,2 +56,40 @@ ## Usage | ||
</fest:template> | ||
``` | ||
``` | ||
## Proxy | ||
Proxy is a connect middleware. It handles requests to the templates, compiles them and wraps results | ||
into AMD module, JSONP or any other format. | ||
AMD: | ||
```javascript | ||
app.use(require('artist').proxy({ | ||
format: 'amd', // 'amd', 'jsonp' or any string with #ID# and #FN# placeholders | ||
route: '/scripts/templates', // middleware mounting point | ||
templates: app.get('views') // path to the view directory | ||
})); | ||
``` | ||
GET /scripts/templates/layout.js | ||
```javascript | ||
define(function(){return function(__fest_context){ | ||
... | ||
}}); | ||
``` | ||
JSONP: | ||
```javascript | ||
app.use(require('artist').proxy({ | ||
format: 'jsonp', | ||
route: '/scripts/templates', | ||
templates: app.get('views') | ||
})); | ||
``` | ||
GET /scripts/templates/layout.js?jsonp=handleTemplate | ||
```javascript | ||
handleTemplate(function(__fest_context){ | ||
... | ||
}); | ||
``` |
var assert = require('assert'), | ||
artist = require('..'); | ||
artist = require('..'), | ||
http = require('./http'); | ||
describe("api", function() { | ||
describe(".render(opts)", function () { | ||
it("should invoke the function with null and a string", function () { | ||
@@ -29,2 +31,3 @@ var errors = [], | ||
}); | ||
it("should invoke the function with an error when the file doesn't exist", function () { | ||
@@ -45,2 +48,3 @@ var errors = [], | ||
}); | ||
it("should invoke the function with an error when the file contains syntax errors", function () { | ||
@@ -61,2 +65,3 @@ var errors = [], | ||
}); | ||
it("should log an message when the file contains runtime error", function () { | ||
@@ -80,2 +85,3 @@ var errors = [], | ||
describe(".renderSync(opts)", function () { | ||
it("should return a string", function () { | ||
@@ -96,2 +102,3 @@ var errors = [], | ||
}); | ||
it("should throw an error when the file doesn't exist", function () { | ||
@@ -112,2 +119,3 @@ var errors = [], | ||
}); | ||
it("should throw an error when the file contains syntax errors", function () { | ||
@@ -122,6 +130,7 @@ var errors = [], | ||
render(__dirname + '/error-syntax.xml', {}); | ||
}, 'Unexpected close tag'); | ||
}, /Unexpected close tag/); | ||
// no runtime errors | ||
assert.deepEqual(errors, []); | ||
}); | ||
it("should log an message when the file contains runtime error", function () { | ||
@@ -140,2 +149,159 @@ var errors = [], | ||
}); | ||
describe(".proxy(opts)", function () { | ||
function performRequest(proxy, request, callback) { | ||
var req = new http.Request(request), | ||
res = new http.Response(); | ||
res.on('finish', function () { | ||
callback(null, res); | ||
}); | ||
proxy(req, res, function (err) { | ||
callback(err, res); | ||
}); | ||
} | ||
var defaultProxy = artist.proxy({ | ||
// format: 'amd', | ||
route: '/scripts', | ||
templates: __dirname | ||
}); | ||
var cacheProxy = artist.proxy({ | ||
cache: true, | ||
route: '/scripts', | ||
templates: __dirname | ||
}); | ||
var jsonpProxy = artist.proxy({ | ||
format: 'jsonp', | ||
route: '/scripts', | ||
templates: __dirname, | ||
maxAge: 86400000 | ||
}); | ||
var festProxy = artist.proxy({ | ||
format: ';(function(x){if(!x.fest)x.fest={};x.fest["#ID#"]=#FN#})(this);', | ||
route: '/scripts', | ||
templates: __dirname | ||
}); | ||
it("should throw an exception if required parameters are missing", function () { | ||
assert.throws(function () { | ||
artist.proxy() | ||
}, /route is required/); | ||
assert.throws(function () { | ||
artist.proxy({route: '/'}) | ||
}, /tempates is required/); | ||
}); | ||
it("should format the function into AMD module by default and prevent browser cache", function (done) { | ||
performRequest(defaultProxy, { | ||
method: 'GET', | ||
url: '/scripts/test.js' | ||
}, function (err, res) { | ||
assert(res.finished); | ||
assert.equal(res.statusCode, 200); | ||
assert.equal(res._headers['cache-control'], 'public, max-age=0'); | ||
assert(res.match(/^define\(function\(\)\{return [\s\S]*\}\);$/)); | ||
done(err); | ||
}); | ||
}); | ||
it("should format the function into JSONP reponse and send custom Cache-Control header", function (done) { | ||
performRequest(jsonpProxy, { | ||
method: 'GET', | ||
url: '/scripts/test.js?jsonp=cb1' | ||
}, function (err, res) { | ||
assert(res.finished); | ||
assert.equal(res.statusCode, 200); | ||
assert(res.match(/^cb1\([\s\S]*\);$/)); | ||
assert.equal(res._headers['cache-control'], 'public, max-age=86400000'); | ||
done(err); | ||
}); | ||
}); | ||
it("should format the function into JSONP response with different jsonp values", function (done) { | ||
performRequest(jsonpProxy, { | ||
method: 'GET', | ||
url: '/scripts/test.js?jsonp=cb2' | ||
}, function (err, res) { | ||
assert(res.finished); | ||
assert.equal(res.statusCode, 200); | ||
assert(res.match(/^cb2\([\s\S]*\);$/)); | ||
done(err); | ||
}); | ||
}); | ||
it("should format the function into custom layout", function (done) { | ||
performRequest(festProxy, { | ||
method: 'GET', | ||
url: '/scripts/test.js' | ||
}, function (err, res) { | ||
assert(res.finished); | ||
assert.equal(res.statusCode, 200); | ||
assert(res.match(/^;\(function\(x\)\{if\(!x\.fest\)x\.fest=\{\};x\.fest\["test"\]=[\s\S]*\}\)\(this\);$/)); | ||
done(err); | ||
}); | ||
}); | ||
it("should just call next() when the route mismatch", function (done) { | ||
performRequest(defaultProxy, { | ||
method: 'GET', | ||
url: '/templates/test.js' | ||
}, function (err, res) { | ||
assert(!res.finished); | ||
done(err); | ||
}); | ||
}); | ||
it("should deal with HEAD requests", function (done) { | ||
performRequest(defaultProxy, { | ||
method: 'HEAD', | ||
url: '/scripts/test.js' | ||
}, function (err, res) { | ||
assert(res.finished); | ||
assert.equal(res.statusCode, 200); | ||
assert(res.match('')); | ||
done(err); | ||
}); | ||
}); | ||
it("should deal with HEAD and GET requests only", function (done) { | ||
performRequest(defaultProxy, { | ||
method: 'POST', | ||
url: '/scripts/test.js' | ||
}, function (err, res) { | ||
assert(!res.finished); | ||
done(err); | ||
}); | ||
}); | ||
it("should deal with If-Modified-Since header", function (done) { | ||
performRequest(cacheProxy, { | ||
headers: {'if-modified-since': (new Date).toUTCString()}, | ||
method: 'GET', | ||
url: '/scripts/test.js' | ||
}, function (err, res) { | ||
assert(res.finished); | ||
assert.equal(res.statusCode, 200); | ||
assert(!res.match('')); | ||
if (err) { | ||
done(err); | ||
} else { | ||
performRequest(cacheProxy, { | ||
headers: {'if-modified-since': (new Date(Date.now() + 60 * 1000)).toUTCString()}, | ||
method: 'GET', | ||
url: '/scripts/test.js' | ||
}, function (err, res) { | ||
assert(res.finished); | ||
assert.equal(res.statusCode, 304); | ||
assert(res.match('')); | ||
done(err); | ||
}); | ||
} | ||
}); | ||
}); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
23059
12
507
93
3
3