Skip to content

Commit

Permalink
Upgrade to Handlebars 3.0
Browse files Browse the repository at this point in the history
This upgrades to Handlebars 3.0 and makes backwards-incompatible
changes to the ExpressHandlebars API. Since Handlebars had a major
version bump, it seemed appropriate for this package to have a major
version bump as well, so I took the liberty of refactoring some things.

- Add `compilerOptions` config property which is passed along to
  `Handlebars.compile()` and `precompile()`.

- Expose ExpressHandlebars metadata to the `data` channel during
  render (#101). This metadata is accessibile via
  `@data.ExpressHandlebars.*`

- New "protected" hooks for AOP-ing template compilation and rendering,
  all of which can optionally return a Promise:

  - `_compileTemplate()`
  - `_precompileTemplate()`
  - `_renderTemplate()`

For most apps this will be a drop-in replacement to v1 of this package,
and using Handlebars 3.0 should be a drop-in replacement as well. That
said, here's the changes that potentially break back-compat:

- Removed using `prototype` props for default config values.

- Removed `handlebarsVersion` from instances and `getHandlebarsSemver`
  static function on the `ExpressHandlebars` constructor.

- Replaced undocumented `compileTemplate()` hook with the protected but
  supported `_compileTemplate()` and `_precompileTemplate()` hooks.
  • Loading branch information
ericf committed Feb 12, 2015
1 parent 1db1174 commit 5b90ad9
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{{{body}}}

{{! Only needs the Handlebars Runtime, because the exposed templates are precompiled. }}
<script src="https://cdn.rawgit.com/components/handlebars.js/v2.0.0/handlebars.runtime.min.js"></script>
<script src="//cdn.jsdelivr.net/handlebarsjs/3.0.0/handlebars.runtime.min.js"></script>
<script>
(function () {
var revive = Handlebars.template,
Expand Down
216 changes: 104 additions & 112 deletions lib/express-handlebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,50 @@

var Promise = global.Promise || require('promise');

var glob = require('glob'),
Handlebars = require('handlebars'),
fs = require('graceful-fs'),
path = require('path'),
semver = require('semver'),
utils = require('./utils');
var glob = require('glob');
var Handlebars = require('handlebars');
var fs = require('graceful-fs');
var path = require('path');

var utils = require('./utils');

module.exports = ExpressHandlebars;

// -----------------------------------------------------------------------------

function ExpressHandlebars(config) {
config || (config = {});

this.handlebars = config.handlebars || this.handlebars;
this.extname = config.extname || this.extname;
this.layoutsDir = config.layoutsDir || this.layoutsDir;
this.partialsDir = config.partialsDir || this.partialsDir;

this.handlebarsVersion =
ExpressHandlebars.getHandlebarsSemver(this.handlebars);

// Config properties with defaults.
utils.assign(this, {
handlebars : Handlebars,
extname : '.handlebars',
layoutsDir : 'views/layouts/',
partialsDir : 'views/partials/',
defaultLayout : undefined,
helpers : null,
compilerOptions: null,
}, config);

// Normalize `extname`.
if (this.extname.charAt(0) !== '.') {
this.extname = '.' + this.extname;
}

this.defaultLayout = config.defaultLayout;
this.helpers = config.helpers;

this.compiled = {};
this.precompiled = {};
// Internal caches of compiled and precompiled templates.
this.compiled = Object.create(null);
this.precompiled = Object.create(null);

// Express view engine integration point.
this.engine = this.renderView.bind(this);
}

ExpressHandlebars._fsCache = {};

ExpressHandlebars.getHandlebarsSemver = function (handlebars) {
var version = handlebars.VERSION || '';

// Makes sure the Handlebars version is a valid semver.
if (version && !semver.valid(version)) {
version = version.replace(/(\d\.\d)\.(\D.*)/, '$1.0-$2');
}

return version;
};

ExpressHandlebars.prototype.handlebars = Handlebars;
ExpressHandlebars.prototype.extname = '.handlebars';
ExpressHandlebars.prototype.layoutsDir = 'views/layouts/';
ExpressHandlebars.prototype.partialsDir = 'views/partials/';

ExpressHandlebars.prototype.compileTemplate = function (template, options) {
options || (options = {});

var compiler = options.precompiled ? 'precompile' : 'compile',
compile = this.handlebars[compiler];

return compile(template);
};

ExpressHandlebars.prototype.getPartials = function (options) {
options || (options = {});

var partialsDirs = Array.isArray(this.partialsDir) ?
this.partialsDir : [this.partialsDir];

partialsDirs = partialsDirs.map(function (dir) {
var dirPath,
dirTemplates,
dirNamespace;
var dirPath;
var dirTemplates;
var dirNamespace;

// Support `partialsDir` collection with object entries that contain a
// templates promise and a namespace.
Expand All @@ -97,7 +69,7 @@ ExpressHandlebars.prototype.getPartials = function (options) {
return templatesPromise.then(function (templates) {
return {
templates: templates,
namespace: dirNamespace
namespace: dirNamespace,
};
});
}, this);
Expand All @@ -106,12 +78,12 @@ ExpressHandlebars.prototype.getPartials = function (options) {
var getPartialName = this._getPartialName.bind(this);

return dirs.reduce(function (partials, dir) {
var templates = dir.templates,
namespace = dir.namespace,
filePaths = Object.keys(templates);
var templates = dir.templates;
var namespace = dir.namespace;
var filePaths = Object.keys(templates);

filePaths.forEach(function (filePath) {
var partialName = getPartialName(filePath, namespace);
var partialName = getPartialName(filePath, namespace);
partials[partialName] = templates[filePath];
});

Expand All @@ -124,19 +96,23 @@ ExpressHandlebars.prototype.getTemplate = function (filePath, options) {
filePath = path.resolve(filePath);
options || (options = {});

var precompiled = options.precompiled,
cache = precompiled ? this.precompiled : this.compiled,
template = options.cache && cache[filePath];
var precompiled = options.precompiled;
var cache = precompiled ? this.precompiled : this.compiled;
var template = options.cache && cache[filePath];

if (template) {
return template;
}

// Optimistically cache template promise to reduce file system I/O, but
// remove from cache if there was a problem.
template = cache[filePath] = this._getFile(filePath, options)
template = cache[filePath] = this._getFile(filePath, {cache: options.cache})
.then(function (file) {
return this.compileTemplate(file, options);
if (precompiled) {
return this._precompileTemplate(file, this.compilerOptions);
}

return this._compileTemplate(file, this.compilerOptions);
}.bind(this));

return template.catch(function (err) {
Expand All @@ -147,16 +123,17 @@ ExpressHandlebars.prototype.getTemplate = function (filePath, options) {

ExpressHandlebars.prototype.getTemplates = function (dirPath, options) {
options || (options = {});
var cache = options.cache;

return this._getDir(dirPath, options).then(function (filePaths) {
return this._getDir(dirPath, {cache: cache}).then(function (filePaths) {
var templates = filePaths.map(function (filePath) {
return this.getTemplate(path.join(dirPath, filePath), options);
}, this);

return Promise.all(templates).then(function (templates) {
return filePaths.reduce(function (map, filePath, i) {
map[filePath] = templates[i];
return map;
return filePaths.reduce(function (hash, filePath, i) {
hash[filePath] = templates[i];
return hash;
}, {});
});
}.bind(this));
Expand All @@ -165,58 +142,65 @@ ExpressHandlebars.prototype.getTemplates = function (dirPath, options) {
ExpressHandlebars.prototype.render = function (filePath, context, options) {
options || (options = {});

// Force `precompiled` to `false` since we're rendering to HTML.
if (options.precompiled) {
options = utils.extend({}, options, {precompiled: false});
}

return Promise.all([
this.getTemplate(filePath, options),
options.partials || this.getPartials(options)
this.getTemplate(filePath, {cache: options.cache}),
options.partials || this.getPartials({cache: options.cache})
]).then(function (templates) {
var template = templates[0],
partials = templates[1],
data = options.data;
var template = templates[0];
var partials = templates[1];

var helpers = options.helpers ||
utils.extend({}, this.handlebars.helpers, this.helpers);
utils.assign({}, this.handlebars.helpers, this.helpers);

// Add ExpressHandlebars metadata to the data channel so that it's
// accessible within the templates and helpers, namespaced under:
// `@data.ExpressHandlebars.*`
var data = utils.assign({}, options.data, {
ExpressHandlebars: utils.assign({}, options, {
filePath: filePath,
helpers : helpers,
partials: partials,
}),
});

return this._renderTemplate(template, context, {
data : data,
helpers : helpers,
partials: partials
partials: partials,
});
}.bind(this));
};

ExpressHandlebars.prototype.renderView = function (viewPath, options, callback) {
var context = options,
data = options.data;
options || (options = {});

var context = options;

var helpers = utils.extend({},
// Merge all sources of helpers together.
var helpers = utils.assign({},
this.handlebars.helpers, this.helpers, options.helpers);

// Pluck-out ExpressHandlebars-specific options.
// Pluck-out ExpressHandlebars-specific options and Handlebars-specific
// rendering options.
options = {
cache : options.cache,
layout : 'layout' in options ? options.layout : this.defaultLayout,
precompiled: false
};
cache : options.cache,
layout: 'layout' in options ? options.layout : this.defaultLayout,

// Extend `options` with Handlebars-specific rendering options.
utils.extend(options, {
data : data,
data : options.data,
helpers : helpers,
partials: this.getPartials(options)
});
partials: this.getPartials({cache: options.cache}),
};

this.render(viewPath, context, options)
.then(function (body) {
var layoutPath = this._resolveLayoutPath(options.layout);

if (layoutPath) {
context = utils.extend({}, context, {body: body});
return this.render(layoutPath, context, options);
return this.render(
layoutPath,
utils.assign({}, context, {body: body}),
utils.assign({}, options, {layout: undefined})
);
}

return body;
Expand All @@ -225,11 +209,30 @@ ExpressHandlebars.prototype.renderView = function (viewPath, options, callback)
.catch(utils.passError(callback));
};

// -- Protected ----------------------------------------------------------------

ExpressHandlebars.prototype._compileTemplate = function (template, options) {
return this.handlebars.compile(template, options);
};

ExpressHandlebars.prototype._precompileTemplate = function (template, options) {
return this.handlebars.precompile(template, options);
};

ExpressHandlebars.prototype._renderTemplate = function (template, context, options) {
return template(context, options);
};

// -- Private ------------------------------------------------------------------

ExpressHandlebars._fsCache = Object.create(null);

ExpressHandlebars.prototype._getDir = function (dirPath, options) {
dirPath = path.resolve(dirPath);
options || (options = {});

var cache = ExpressHandlebars._fsCache,
dir = options.cache && cache[dirPath];
var cache = ExpressHandlebars._fsCache;
var dir = options.cache && cache[dirPath];

if (dir) {
return dir.then(function (dir) {
Expand Down Expand Up @@ -261,9 +264,10 @@ ExpressHandlebars.prototype._getDir = function (dirPath, options) {

ExpressHandlebars.prototype._getFile = function (filePath, options) {
filePath = path.resolve(filePath);
options || (options = {});

var cache = ExpressHandlebars._fsCache,
file = options.cache && cache[filePath];
var cache = ExpressHandlebars._fsCache;
var file = options.cache && cache[filePath];

if (file) {
return file;
Expand All @@ -288,28 +292,16 @@ ExpressHandlebars.prototype._getFile = function (filePath, options) {
};

ExpressHandlebars.prototype._getPartialName = function (filePath, namespace) {
var extRegex = new RegExp(this.extname + '$'),
name = filePath.replace(extRegex, ''),
version = this.handlebarsVersion;
var extRegex = new RegExp(this.extname + '$');
var name = filePath.replace(extRegex, '');

if (namespace) {
name = namespace + '/' + name;
}

// Fixes a Handlebars bug in versions prior to 1.0.rc.2 which caused
// partials with "/"s in their name to not be found.
// https://github.com/wycats/handlebars.js/pull/389
if (version && !semver.satisfies(version, '>=1.0.0-rc.2')) {
name = name.replace(/\//g, '.');
}

return name;
};

ExpressHandlebars.prototype._renderTemplate = function (template, context, options) {
return template(context, options);
};

ExpressHandlebars.prototype._resolveLayoutPath = function (layoutPath) {
if (!layoutPath) {
return null;
Expand Down
16 changes: 1 addition & 15 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
'use strict';

exports.extend = extend;
exports.assign = Object.assign || require('object.assign');
exports.passError = passError;
exports.passValue = passValue;

// -----------------------------------------------------------------------------

function extend(target) {
[].slice.call(arguments, 1).forEach(function (source) {
if (!source) { return; }

for (var key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
});

return target;
}

function passError(callback) {
return function (reason) {
setImmediate(function () {
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
"dependencies": {
"glob": "^4.0.3",
"graceful-fs": "^3.0.2",
"handlebars": "^2.0.0",
"promise": "^6.0.0",
"semver": "^3.0.1"
"handlebars": "^3.0.0",
"object.assign": "^1.1.1",
"promise": "^6.0.0"
},
"main": "index.js",
"directories": {
Expand Down

0 comments on commit 5b90ad9

Please sign in to comment.