diff --git a/HISTORY.md b/HISTORY.md index d4cd379..4c35c55 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,11 @@ Express Handlebars Change History ================================= +3.1.0 (2016-12-08) +------------------ +* Add capability to render network based templates and partials. + (All changes are backwards compatible) (@andy9775) + 3.0.0 (2016-01-26) ------------------ diff --git a/README.md b/README.md index dc099d3..49f0662 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ After building a half-dozen Express apps, I developed requirements and opinions * Ability to use a different Handlebars module/implementation other than the Handlebars npm package. +* Ability to fetch templates from the network. This is useful when using webpack dev server with Hot Module Reloading and generating a custom template via HtmlWebpackPlugin. It can also be used in production if you have your handlebars templates on a remote server. + ### Package Design This package was designed to work great for both the simple and complex use cases. I _intentionally_ made sure the full implementation is exposed and is easily overridable. @@ -56,6 +58,8 @@ This exported engine factory has two properties which expose the underlying impl An instance-based approach is used so that multiple `ExpressHandlebars` instances can be created with their own configuration, templates, partials, and helpers. +Further, if loading templates from the network a custom View resolver should be set. This is defined as `NetworkView` and can be required using `require('express-handlebars').NetworkView` or `import { NetworkView } from 'express-handlebars'` if using ES2015 syntax. + ## Installation @@ -142,6 +146,37 @@ $ npm install $ npm start ``` +### Advanced Usage with network based templates + +```javascript +var express = require('express'); +var exphbs = require('express-handlebars'); +var NetworkView = require('express-handlebars').NetworkView + +var app = express(); + +app.engine('handlebars', exphbs({ + layoutsAddress: 'http://localhost:3000/dist', + partialsAddress: [ + { + path: 'http://localhost:3000/dist', + templates: [ + 'title.handlebars', + 'body.handlebars' + ] + } + ] +})); +app.set('view engine', 'handlebars'); +app.set('view', NetworkView); // must be set to use network based templates + +app.get('/', function (req, res) { + res.render('home'); +}); + +app.listen(3000); +``` + ### Using Instances Another way to use this view engine is to create an instance(s) of `ExpressHandlebars`, allowing access to the full API: @@ -342,6 +377,10 @@ The string path to the directory where the layout templates reside. **Note:** If you configure Express to look for views in a custom location (e.g., `app.set('views', 'some/path/')`), you will need to reflect that by passing an updated path as the `layoutsDir` property in your configuration. +#### `layoutsAddress` +The string path to the URL endpoint where the layout templates reside. +**Note:** If you configure Express to look for views in a custom location using `app.set('views', '...')`, you will need to pass the same path to `layoutsAddress`. + #### `partialsDir="views/partials/"` The string path to the directory where the partials templates reside or object with the following properties: @@ -353,6 +392,14 @@ The string path to the directory where the partials templates reside or object w **Note:** Multiple partials dirs can be used by making `partialsDir` an array of strings, and/or config objects as described above. The namespacing feature is useful if multiple partials dirs are used and their file paths might clash. +#### `partialsAddress` +The URL where the partials templates reside or an object with the following properties: + +* `path`: The string URL to the location of partial templates. Can be a single template location or the URL endpoint containing the array of templates defined under `templates`. +* `namespace`: Optional string namespace to prefix the partial names. +* `templates`: Optional collection of templates names in the form `['index.handlebars', 'home.handlebars']`. If using multiple templates defined at one endpoint, they must all be defined here - we don't have the ability to traverse the files available at the URL. +Further, requests are made without modifying the template names. If you provide `[index, home]` then we will make a request to `/index` or `/home`. If `[index.handlebars, home.handlebars]` is provided, the request is made to `/index.handlebars` and `/home.handlebars` - therefore providing the file extension is critical. It is also not possible to fetch all the partials in one go - each parial listed is fetched one by one. However, if configured parials are cached locally. + #### `defaultLayout` The string name or path of a template in the `layoutsDir` to use as the default layout. This is overridden by a `layout` specified in the app or response `locals`. **Note:** A falsy value will render without a layout; e.g., `res.render('home', {layout: false});`. @@ -420,6 +467,20 @@ hbs.getPartials().then(function (partials) { // => title: [Function] } }); ``` +#### `getRemotePartials([options])` +Retrieves the partials in the `partialsAddress` and returns a Promise for an object mapping the partials in the form `{name: partial}`. + +By default each partial will be a compiled Handlebars template function. Use `options.precompiled` to receive the partials as precompiled templates — this is useful for sharing templates with client code. + +**Parameters:** + +* `[options]`: Optional object containing any of the following properties: + + * `[cache]`: Whether cached templates can be used if they have already been requested. This is recommended for production to avoid unnecessary network I/O. + + * `[precompiled=false]`: Whether precompiled templates should be provided, instead of compiled Handlebars template functions. + +If using a single `partialsAddress` then it must specify the exact template to use as we cannot traverse the files available at a defined URL. If using an array of objects to define the partials, each object should define a `path` defining the URL and `templates` which should contain an array of partials avaiable at the defined `path`. #### `getTemplate(filePath, [options])` Retrieves the template at the specified `filePath` and returns a Promise for the compiled Handlebars template function. @@ -436,6 +497,21 @@ Use `options.precompiled` to receive a precompiled Handlebars template. * `[precompiled=false]`: Whether a precompiled template should be provided, instead of a compiled Handlebars template function. +### `getRemoteTemplate(path, [options])` +Retrieves the template specifed at `path` from a network resource and returns a Promise for the compiled Handlebars template function. + +Use `options.precompiled` to receive a precompiled Handlebars template. + +**Parameters:** + +* `path`: String URL to the Handlebars template file. + +* `[options]`: Optional object containing any of the following properties: + + * `[cache]`: Whether a cached template can be used if it have already been requested. This is recommended for production to avoid necessary network I/O. + + * `[precompiled=false]`: Whether a precompiled template should be provided, instead of a compiled Handlebars template function. + #### `getTemplates(dirPath, [options])` Retrieves all the templates in the specified `dirPath` and returns a Promise for an object mapping the compiled templates in the form `{filename: template}`. @@ -451,7 +527,25 @@ Use `options.precompiled` to receive precompiled Handlebars templates — this i * `[precompiled=false]`: Whether precompiled templates should be provided, instead of a compiled Handlebars template function. -#### `render(filePath, context, [options])` +### `getRemoteTemplates(path, templates, options)` +Retrieves all the templates specifed in the `templates` argument at the `path` URL. Returns a Promise for an object mapping the compiled templates in the form `{filename: template}`. + +Use `options.precompiled` to receive precompiled Handlebars templates — this is useful for sharing templates with client code. + +**Parameters:** + +* `path`: String path to the directory containing Handlebars template files. + +* `templates`: String array of templates to fetch. + +* `[options]`: Optional object containing any of the following properties: + + * `[cache]`: Whether cached templates can be used if it have already been requested. This is recommended for production to avoid unnecessary network I/O. + + * `[precompiled=false]`: Whether precompiled templates should be provided, instead of a compiled Handlebars template function. + + +#### `renderFromFile(filePath, context, [options])` Renders the template at the specified `filePath` with the `context`, using this instance's `helpers` and partials by default, and returns a Promise for the resulting string. **Parameters:** @@ -470,10 +564,27 @@ Renders the template at the specified `filePath` with the `context`, using this * `[partials]`: Render-level partials that will be used instead of any instance-level partials. This is used internally as an optimization to avoid re-loading all the partials. -#### `renderView(viewPath, options|callback, [callback])` -Renders the template at the specified `viewPath` as the `{{{body}}}` within the layout specified by the `defaultLayout` or `options.layout`. Rendering will use this instance's `helpers` and partials, and passes the resulting string to the `callback`. +### `renderFromRemote(path, context, [options])` +Renders the template at the specifed `path` from the network with the `context`, using this instance's `helpers` and partials by default, and returns a Promise for the resulting string. -This method is called by Express and is the main entry point into this Express view engine implementation. It adds the concept of a "layout" and delegates rendering to the `render()` method. +**Parameters:** + +* `path`: String path to the Handlebars template file - a URL. + +* `context`: Object in which the template will be executed. This contains all of the values to fill into the template. + +* `[options]`: Optional object which can contain any of the following properties which affect this view engine's behavior: + + * `[cache]`: Whether a cached template can be used if it have already been requested. This is recommended for production to avoid unnecessary network I/O. + + * `[data]`: Optional object which can contain any data that Handlebars will pipe through the template, all helpers, and all partials. This is a side data channel. + + * `[helpers]`: Render-level helpers that will be used instead of any instance-level helpers; these will be merged with (and will override) any global Handlebars helper functions. + + * `[partials]`: Render-level partials that will be used instead of any instance-level partials. This is used internally as an optimization to avoid re-loading all the partials. + +#### `renderLocalView(viewPath, options|callback, [callback])` +Renders the template at the specified `viewPath` as the `{{{body}}}` within the layout specified by the `defaultLayout` or `options.layout`. Rendering will use this instance's `helpers` and partials, and passes the resulting string to the `callback`. The `options` will be used both as the context in which the Handlebars templates are rendered, and to signal this view engine on how it should behave, e.g., `options.cache=false` will load _always_ load the templates from disk. @@ -495,6 +606,34 @@ The `options` will be used both as the context in which the Handlebars templates * `callback`: Function to call once the template is retrieved. +#### `renderRemoteView(viewPath, options, callback)` +Renders the template at the specified `viewPath` as the `{{{body}}}` within the layout specified by the `defaultLayout` or `options.layout`. Rendering will use this instance's `helpers` and partials, and passes the resulting string to the `callback`. This method fetches the templates from a defined network resource rather than a local directory. + +The `options` will be used both as the context in which the Handlebars templates are rendered, and to signal this view engine on how it should behave, e.g., `options.cache=false` will load _always_ load the templates from the network. + +**Parameters:** + +* `viewPath`: String path to the URL endpoint which to fetch the Handlebars template file which should serve as the `{{{body}}}` when using a layout. + +* `[options]`: Optional object which will serve as the context in which the Handlebars templates are rendered. It may also contain any of the following properties which affect this view engine's behavior: + + * `[cache]`: Whether cached templates can be used if they have already been requested. This is recommended for production to avoid unnecessary network I/O. + + * `[data]`: Optional object which can contain any data that Handlebars will pipe through the template, all helpers, and all partials. This is a side data channel. + + * `[helpers]`: Render-level helpers that will be merged with (and will override) instance and global helper functions. + + * `[partials]`: Render-level partials will be merged with (and will override) instance and global partials. This should be a `{partialName: fn}` hash or a Promise of an object with this shape. + + * `[layout]`: Optional string path to the Handlebars template file to be used as the "layout". This overrides any `defaultLayout` value. Passing a falsy value will render with no layout (even if a `defaultLayout` is defined). + +* `callback`: Function to call once the template is retrieved. + +#### `renderView(viewPath, options|callback, [callback])` +This method is called by Express and is the main entry point into this Express view engine implementation. It adds the concept of a "layout" and delegates rendering to the `render()` method. + +If `layoutsAddress` is defined in the config object, this method uses `renderRemoteView` to render the required tempaltes from a network based resource, else it renders the template from the provided `layoutDir`. + ### Hooks The following is the list of protected methods that are called internally and serve as _hooks_ to override functionality of `ExpressHandlebars` instances. A value or a promise can be returned from these methods which allows them to perform async operations. diff --git a/index.js b/index.js index b1f6dd5..f4ef636 100644 --- a/index.js +++ b/index.js @@ -7,10 +7,12 @@ 'use strict'; var ExpressHandlebars = require('./lib/express-handlebars'); +var NetworkView = require('./lib/networkView'); exports = module.exports = exphbs; exports.create = create; exports.ExpressHandlebars = ExpressHandlebars; +exports.NetworkView = NetworkView; // ----------------------------------------------------------------------------- diff --git a/lib/express-handlebars.js b/lib/express-handlebars.js index af8c656..2cb0c70 100644 --- a/lib/express-handlebars.js +++ b/lib/express-handlebars.js @@ -8,10 +8,11 @@ var Promise = global.Promise || require('promise'); -var glob = require('glob'); +var glob = require('glob'); var Handlebars = require('handlebars'); -var fs = require('graceful-fs'); -var path = require('path'); +var fs = require('graceful-fs'); +var path = require('path'); +var request = require('request'); var utils = require('./utils'); @@ -20,324 +21,553 @@ module.exports = ExpressHandlebars; // ----------------------------------------------------------------------------- function ExpressHandlebars(config) { - // Config properties with defaults. - utils.assign(this, { - handlebars : Handlebars, - extname : '.handlebars', - layoutsDir : 'views/layouts/', - partialsDir : 'views/partials/', - defaultLayout : undefined, - helpers : undefined, - compilerOptions: undefined, - }, config); - - // Express view engine integration point. - this.engine = this.renderView.bind(this); - - // Normalize `extname`. - if (this.extname.charAt(0) !== '.') { - this.extname = '.' + this.extname; - } + // Config properties with defaults. + utils.assign(this, { + handlebars: Handlebars, + extname: '.handlebars', + layoutsDir: 'views/layouts/', + partialsDir: 'views/partials/', + layoutsAddress: undefined, // URL of layouts directory + partialsAddress: undefined, // URL of partials directory + defaultLayout: undefined, + helpers: undefined, + compilerOptions: undefined, + }, config); + + // Express view engine integration point. + this.engine = this.renderView.bind(this); + + // Normalize `extname`. + if (this.extname.charAt(0) !== '.') { + this.extname = '.' + this.extname; + } + + // Internal caches of compiled and precompiled templates. + this.compiled = Object.create(null); + this.precompiled = Object.create(null); + + // Private internal file system cache. + this._fsCache = Object.create(null); +} - // Internal caches of compiled and precompiled templates. - this.compiled = Object.create(null); - this.precompiled = Object.create(null); +ExpressHandlebars.prototype.getRemotePartials = function(options) { + + if (!this.partialsAddress) { + return; + } + + var partialsAddresses = Array.isArray(this.partialsAddress) ? + this.partialsAddress : [this.partialsAddress] + + partialsAddresses = partialsAddresses + .map(function(address) { + var addPath; // URL path of templates + var addTemplates; // array of template names + var addNamespace; + + if (typeof address === 'string') { + /* + assume the full address with url is passed in + e.g. http://localhost:3000/partials/title.handlebars + */ + address = address.endsWith('/') ? + address.substr(0, address.length - 1) : address + addTemplates = [address.substr(address.lastIndexOf('/') + 1)]; + addPath = address.substr(0, address.lastIndexOf('/')) + '/'; + + } else if (typeof address === 'object') { + addTemplates = address.templates; + addNamespace = address.namespace; + addPath = address.path.endsWith('/') ? + address.path : address.path + '/'; + } + + /* + Because we cannot fetch a list of templates inside a remote directory, + the partials object should contain a list of templates for each path. + We do not traverse the partials path to fetch all partial templates! + */ + if (!addPath) { + throw new Error('A partials address must be a string or config object'); + } + if (!addTemplates) { + throw new Error('A partials object should have partial templates defined'); + } + + return Promise.resolve( + this.getRemoteTemplates(addPath, addTemplates, options)) + .then(function(templates) { + return { + templates: templates, + nameSpace: addNamespace, + } + }); + }, this); + + return Promise.all(partialsAddresses) + .then(function(addresses) { + var getTemplateName = this._getTemplateName.bind(this); + + return addresses + .reduce(function(partials, address) { + var templates = address.templates; + var namespace = address.nameSpace; + var addressPaths = Object.keys(templates); + + addressPaths + .forEach(function(addressPath) { + var partialName = getTemplateName(addressPath, namespace); + partials[partialName] = templates[addressPath]; + }); - // Private internal file system cache. - this._fsCache = Object.create(null); + return partials; + }, {}); + }.bind(this)); } -ExpressHandlebars.prototype.getPartials = function (options) { - var partialsDirs = Array.isArray(this.partialsDir) ? - this.partialsDir : [this.partialsDir]; - - partialsDirs = partialsDirs.map(function (dir) { - var dirPath; - var dirTemplates; - var dirNamespace; - - // Support `partialsDir` collection with object entries that contain a - // templates promise and a namespace. - if (typeof dir === 'string') { - dirPath = dir; - } else if (typeof dir === 'object') { - dirTemplates = dir.templates; - dirNamespace = dir.namespace; - dirPath = dir.dir; - } - - // We must have some path to templates, or templates themselves. - if (!(dirPath || dirTemplates)) { - throw new Error('A partials dir must be a string or config object'); - } - - // Make sure we're have a promise for the templates. - var templatesPromise = dirTemplates ? Promise.resolve(dirTemplates) : - this.getTemplates(dirPath, options); - - return templatesPromise.then(function (templates) { - return { - templates: templates, - namespace: dirNamespace, - }; - }); - }, this); +ExpressHandlebars.prototype.getPartials = function(options) { + var partialsDirs = Array.isArray(this.partialsDir) ? + this.partialsDir : [this.partialsDir]; + + partialsDirs = partialsDirs.map(function(dir) { + var dirPath; + var dirTemplates; + var dirNamespace; + + // Support `partialsDir` collection with object entries that contain a + // templates promise and a namespace. + if (typeof dir === 'string') { + dirPath = dir; + } else if (typeof dir === 'object') { + dirTemplates = dir.templates; + dirNamespace = dir.namespace; + dirPath = dir.dir; + } - return Promise.all(partialsDirs).then(function (dirs) { - var getTemplateName = this._getTemplateName.bind(this); + // We must have some path to templates, or templates themselves. + if (!(dirPath || dirTemplates)) { + throw new Error('A partials dir must be a string or config object'); + } - return dirs.reduce(function (partials, dir) { - var templates = dir.templates; - var namespace = dir.namespace; - var filePaths = Object.keys(templates); + // Make sure we're have a promise for the templates. + var templatesPromise = dirTemplates ? Promise.resolve(dirTemplates) : + this.getTemplates(dirPath, options); - filePaths.forEach(function (filePath) { - var partialName = getTemplateName(filePath, namespace); - partials[partialName] = templates[filePath]; - }); + return templatesPromise.then(function(templates) { + return { + templates: templates, + namespace: dirNamespace, + }; + }); + }, this); - return partials; - }, {}); - }.bind(this)); -}; + return Promise.all(partialsDirs).then(function(dirs) { + var getTemplateName = this._getTemplateName.bind(this); -ExpressHandlebars.prototype.getTemplate = function (filePath, options) { - filePath = path.resolve(filePath); - options || (options = {}); + return dirs.reduce(function(partials, dir) { + var templates = dir.templates; + var namespace = dir.namespace; + var filePaths = Object.keys(templates); - var precompiled = options.precompiled; - var cache = precompiled ? this.precompiled : this.compiled; - var template = options.cache && cache[filePath]; + filePaths.forEach(function(filePath) { + var partialName = getTemplateName(filePath, namespace); + partials[partialName] = templates[filePath]; + }); - if (template) { - return template; - } + return partials; + }, {}); + }.bind(this)); +}; - // 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, {cache: options.cache}) - .then(function (file) { - if (precompiled) { - return this._precompileTemplate(file, this.compilerOptions); - } +ExpressHandlebars.prototype.getTemplate = function(filePath, options) { + filePath = path.resolve(filePath); + options || (options = {}); + + 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, { + cache: options.cache + }) + .then(function(file) { + if (precompiled) { + return this._precompileTemplate(file, this.compilerOptions); + } + + return this._compileTemplate(file, this.compilerOptions); + }.bind(this)); - return this._compileTemplate(file, this.compilerOptions); - }.bind(this)); + return template.catch(function(err) { + delete cache[filePath]; + throw err; + }); +}; - return template.catch(function (err) { - delete cache[filePath]; - throw err; +ExpressHandlebars.prototype.getRemoteTemplate = function(path, options) { + options || (options = {}); + var precompiled = options.precompiled; + var cache = precompiled ? this.precompiled : this.compiled; + var template = options.cache && cache[path]; + + if (template) { + return template; + } + + return cache[path] = this._getRemoteFile(path, {cache: options.cache}) + .then(function(templateBody) { + if (precompiled) { + return this._precompileTemplate(templateBody, this.compilerOptions); + } + return this._compileTemplate(templateBody, this.compilerOptions); + }.bind(this)) + .catch(function(err) { + delete cache[path]; + throw err; }); -}; +} -ExpressHandlebars.prototype.getTemplates = function (dirPath, options) { - options || (options = {}); - var cache = options.cache; +ExpressHandlebars.prototype.getTemplates = function(dirPath, options) { + options || (options = {}); + var cache = options.cache; - 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 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 (hash, filePath, i) { - hash[filePath] = templates[i]; - return hash; - }, {}); - }); - }.bind(this)); + return Promise.all(templates).then(function(templates) { + return filePaths.reduce(function(hash, filePath, i) { + hash[filePath] = templates[i]; + return hash; + }, {}); + }); + }.bind(this)); }; -ExpressHandlebars.prototype.render = function (filePath, context, options) { - options || (options = {}); - - return Promise.all([ - this.getTemplate(filePath, {cache: options.cache}), - options.partials || this.getPartials({cache: options.cache}), - ]).then(function (templates) { - var template = templates[0]; - var partials = templates[1]; - var helpers = options.helpers || this.helpers; - - // Add ExpressHandlebars metadata to the data channel so that it's - // accessible within the templates and helpers, namespaced under: - // `@exphbs.*` - var data = utils.assign({}, options.data, { - exphbs: utils.assign({}, options, { - filePath: filePath, - helpers : helpers, - partials: partials, - }), - }); +ExpressHandlebars.prototype.getRemoteTemplates = function(path, templates, options) { + options || (options = {}); - return this._renderTemplate(template, context, { - data : data, - helpers : helpers, - partials: partials, - }); + var templatesResult = templates + .map(function(template) { + return this.getRemoteTemplate(path + template, options); }.bind(this)); -}; -ExpressHandlebars.prototype.renderView = function (viewPath, options, callback) { - options || (options = {}); - - var context = options; - - // Express provides `settings.views` which is the path to the views dir that - // the developer set on the Express app. When this value exists, it's used - // to compute the view's name. Layouts and Partials directories are relative - // to `settings.view` path - var view; - var viewsPath = options.settings && options.settings.views; - if (viewsPath) { - view = this._getTemplateName(path.relative(viewsPath, viewPath)); - this.partialsDir = path.join(viewsPath, 'partials/'); - this.layoutsDir = path.join(viewsPath, 'layouts/'); - } + return Promise.all(templatesResult) + .then(function(template) { + return templates + .reduce(function(partials, templatePath, i) { + partials[templatePath] = template[i]; + return partials; + }, {}); + }); +} + +ExpressHandlebars.prototype.renderFromRemote = function(path, context, options) { + options || (options = {}); - // Merge render-level and instance-level helpers together. - var helpers = utils.assign({}, this.helpers, options.helpers); + return Promise.all([ + this.getRemoteTemplate(path, {cache: options.cache}), + options.partials || this.getRemotePartials({cache: options.cache}) + ]).then(function(templates) { + var template = templates[0]; + var partials = templates[1]; - // Merge render-level and instance-level partials together. - var partials = Promise.all([ - this.getPartials({cache: options.cache}), - Promise.resolve(options.partials), - ]).then(function (partials) { - return utils.assign.apply(null, [{}].concat(partials)); + var helpers = options.helpers || this.helpers; + + var data = utils.assign({}, options.data, { + exphbs: utils.assign({}, options, { + filePath: path, + helpers: helpers, + partials: partials + }), }); - // Pluck-out ExpressHandlebars-specific options and Handlebars-specific - // rendering options. - options = { - cache : options.cache, - view : view, - layout: 'layout' in options ? options.layout : this.defaultLayout, + return this._renderTemplate(template, context, { + data: data, + helpers: helpers, + partials: partials, + }); + }.bind(this)); +} - data : options.data, - helpers : helpers, +ExpressHandlebars.prototype.renderFromFile = function(filePath, context, options) { + options || (options = {}); + + return Promise.all([ + this.getTemplate(filePath, { + cache: options.cache + }), + options.partials || this.getPartials({ + cache: options.cache + }), + ]).then(function(templates) { + var template = templates[0]; + var partials = templates[1]; + var helpers = options.helpers || this.helpers; + + // Add ExpressHandlebars metadata to the data channel so that it's + // accessible within the templates and helpers, namespaced under: + // `@exphbs.*` + var data = utils.assign({}, options.data, { + exphbs: utils.assign({}, options, { + filePath: filePath, + helpers: helpers, partials: partials, - }; - - this.render(viewPath, context, options) - .then(function (body) { - var layoutPath = this._resolveLayoutPath(options.layout); - - if (layoutPath) { - return this.render( - layoutPath, - utils.assign({}, context, {body: body}), - utils.assign({}, options, {layout: undefined}) - ); - } - - return body; - }.bind(this)) - .then(utils.passValue(callback)) - .catch(utils.passError(callback)); + }), + }); + + return this._renderTemplate(template, context, { + data: data, + helpers: helpers, + partials: partials, + }); + }.bind(this)); +}; + +ExpressHandlebars.prototype.renderRemoteView = function(viewPath, options, callback) { + + var view; // view name e.g. index.handlebars + var viewsPath = options.settings && options.settings.views; // URL to views + + if (viewsPath) { + viewsPath = viewsPath.endsWith('/') ? + viewsPath : (viewsPath + '/'); + view = viewPath.replace(viewsPath, ''); + } + + var helpers = utils.assign({}, this.helpers, options.helpers); + + // Merge render-level and instance-level partials together. + var partials = Promise.all([ + this.getRemotePartials({cache: options.cache}), + Promise.resolve(options.partials), // if cached + ]).then(function(partials) { + return utils.assign.apply(null, [{}].concat(partials)); + }); + + var context = options; + options = { + cache: options.cache, + view: view, + layout: 'layout' in options ? options.layout : this.defaultLayout, + + data: options.data, + helpers: helpers, + partials: partials, + }; + + this.renderFromRemote(viewPath, context, options) + .then(utils.passValue(callback)) + .catch(utils.passError(callback)); +} + +ExpressHandlebars.prototype.renderLocalView = function(viewPath, options, callback) { + options || (options = {}); + + var context = options; + + // Express provides `settings.views` which is the path to the views dir that + // the developer set on the Express app. When this value exists, it's used + // to compute the view's name. Layouts and Partials directories are relative + // to `settings.view` path + var view; + var viewsPath = options.settings && options.settings.views; + if (viewsPath) { + view = this._getTemplateName(path.relative(viewsPath, viewPath)); + this.partialsDir = path.join(viewsPath, 'partials/'); + this.layoutsDir = path.join(viewsPath, 'layouts/'); + } + + // Merge render-level and instance-level helpers together. + var helpers = utils.assign({}, this.helpers, options.helpers); + + // Merge render-level and instance-level partials together. + var partials = Promise.all([ + this.getPartials({ + cache: options.cache + }), + Promise.resolve(options.partials), + ]).then(function(partials) { + return utils.assign.apply(null, [{}].concat(partials)); + }); + + // Pluck-out ExpressHandlebars-specific options and Handlebars-specific + // rendering options. + options = { + cache: options.cache, + view: view, + layout: 'layout' in options ? options.layout : this.defaultLayout, + + data: options.data, + helpers: helpers, + partials: partials, + }; + + + this.renderFromFile(viewPath, context, options) + .then(function(body) { + var layoutPath = this._resolveLayoutPath(options.layout); + + if (layoutPath) { + return this.renderFromFile( + layoutPath, + utils.assign({}, context, { + body: body + }), + utils.assign({}, options, { + layout: undefined + }) + ); + } + + return body; + }.bind(this)) + .then(utils.passValue(callback)) + .catch(utils.passError(callback)); +} + +ExpressHandlebars.prototype.renderView = function(viewPath, options, callback) { + if (this.layoutsAddress) { + this.renderRemoteView(viewPath, options, callback); + } else { + this.renderLocalView(viewPath, options, callback); + } }; // -- Protected Hooks ---------------------------------------------------------- -ExpressHandlebars.prototype._compileTemplate = function (template, options) { - return this.handlebars.compile(template, options); +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._precompileTemplate = function(template, options) { + return this.handlebars.precompile(template, options); }; -ExpressHandlebars.prototype._renderTemplate = function (template, context, options) { - return template(context, options); +ExpressHandlebars.prototype._renderTemplate = function(template, context, options) { + return template(context, options); }; // -- Private ------------------------------------------------------------------ -ExpressHandlebars.prototype._getDir = function (dirPath, options) { - dirPath = path.resolve(dirPath); - options || (options = {}); - - var cache = this._fsCache; - var dir = options.cache && cache[dirPath]; +ExpressHandlebars.prototype._getDir = function(dirPath, options) { + dirPath = path.resolve(dirPath); + options || (options = {}); - if (dir) { - return dir.then(function (dir) { - return dir.concat(); - }); - } + var cache = this._fsCache; + var dir = options.cache && cache[dirPath]; - var pattern = '**/*' + this.extname; - - // Optimistically cache dir promise to reduce file system I/O, but remove - // from cache if there was a problem. - dir = cache[dirPath] = new Promise(function (resolve, reject) { - glob(pattern, { - cwd : dirPath, - follow: true - }, function (err, dir) { - if (err) { - reject(err); - } else { - resolve(dir); - } - }); + if (dir) { + return dir.then(function(dir) { + return dir.concat(); }); - - return dir.then(function (dir) { - return dir.concat(); - }).catch(function (err) { - delete cache[dirPath]; - throw err; + } + + var pattern = '**/*' + this.extname; + + // Optimistically cache dir promise to reduce file system I/O, but remove + // from cache if there was a problem. + dir = cache[dirPath] = new Promise(function(resolve, reject) { + glob(pattern, { + cwd: dirPath, + follow: true + }, function(err, dir) { + if (err) { + reject(err); + } else { + resolve(dir); + } }); + }); + + return dir.then(function(dir) { + return dir.concat(); + }).catch(function(err) { + delete cache[dirPath]; + throw err; + }); }; -ExpressHandlebars.prototype._getFile = function (filePath, options) { - filePath = path.resolve(filePath); - options || (options = {}); - - var cache = this._fsCache; - var file = options.cache && cache[filePath]; - - if (file) { - return file; - } - - // Optimistically cache file promise to reduce file system I/O, but remove - // from cache if there was a problem. - file = cache[filePath] = new Promise(function (resolve, reject) { - fs.readFile(filePath, 'utf8', function (err, file) { - if (err) { - reject(err); - } else { - resolve(file); - } - }); +ExpressHandlebars.prototype._getRemoteFile = function(path, options) { + options || (options = {}); + var cache = this._fsCache; + var file = options.cache && cache[path]; + + if (file) { + return file; + } + + return cache[path] = new Promise(function(resolve, reject) { + request(path, function(err, response, body) { + if (err || response.statusCode !== 200) { + reject(err); + } + resolve(body); + }.bind(this)); + }) + .catch(function(err) { + delete cache[path]; + throw err; }); +} - return file.catch(function (err) { - delete cache[filePath]; - throw err; +ExpressHandlebars.prototype._getFile = function(filePath, options) { + filePath = path.resolve(filePath); + options || (options = {}); + + var cache = this._fsCache; + var file = options.cache && cache[filePath]; + + if (file) { + return file; + } + + // Optimistically cache file promise to reduce file system I/O, but remove + // from cache if there was a problem. + file = cache[filePath] = new Promise(function(resolve, reject) { + fs.readFile(filePath, 'utf8', function(err, file) { + if (err) { + reject(err); + } else { + resolve(file); + } }); + }); + + return file.catch(function(err) { + delete cache[filePath]; + throw err; + }); }; -ExpressHandlebars.prototype._getTemplateName = function (filePath, namespace) { - var extRegex = new RegExp(this.extname + '$'); - var name = filePath.replace(extRegex, ''); +ExpressHandlebars.prototype._getTemplateName = function(filePath, namespace) { + var extRegex = new RegExp(this.extname + '$'); + var name = filePath.replace(extRegex, ''); - if (namespace) { - name = namespace + '/' + name; - } + if (namespace) { + name = namespace + '/' + name; + } - return name; + return name; }; -ExpressHandlebars.prototype._resolveLayoutPath = function (layoutPath) { - if (!layoutPath) { - return null; - } +ExpressHandlebars.prototype._resolveLayoutPath = function(layoutPath) { + if (!layoutPath) { + return null; + } - if (!path.extname(layoutPath)) { - layoutPath += this.extname; - } + if (!path.extname(layoutPath)) { + layoutPath += this.extname; + } - return path.resolve(this.layoutsDir, layoutPath); + return path.resolve(this.layoutsDir, layoutPath); }; diff --git a/lib/networkView.js b/lib/networkView.js new file mode 100644 index 0000000..c3ff473 --- /dev/null +++ b/lib/networkView.js @@ -0,0 +1,21 @@ +var View = require('express/lib/view'); + +var NetworkView = function(name, options) { + View.call(this, name, options); +}; + +NetworkView.prototype = Object.create(View.prototype); +NetworkView.prototype.constructor = NetworkView; + +NetworkView.prototype.lookup = function(name) { + return View.prototype.lookup.call(this, name) || this.join(this.root, name); +} + +NetworkView.prototype.join = function(root, name) { + root = root[root.length - 1] === '/' ? + root.slice(0, root.length - 1) : root; + + return root + (name[0] === '/' ? name : ('/' + name)); +} + +module.exports = NetworkView; diff --git a/package.json b/package.json index 5c8aeb8..2b5b105 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "express-handlebars", "description": "A Handlebars view engine for Express which doesn't suck.", - "version": "3.0.0", + "version": "3.1.0", "homepage": "https://github.com/ericf/express-handlebars", "keywords": [ "express", @@ -24,11 +24,13 @@ "node": ">=0.10" }, "dependencies": { + "express": "^4.14.0", "glob": "^6.0.4", "graceful-fs": "^4.1.2", "handlebars": "^4.0.5", "object.assign": "^4.0.3", - "promise": "^7.0.0" + "promise": "^7.0.0", + "request": "^2.79.0" }, "main": "index.js", "directories": {