From c9bac119dac928a314e778132534272b0f6008bd Mon Sep 17 00:00:00 2001 From: Jeffrey Jagoda Date: Thu, 30 Jul 2015 10:03:43 -0400 Subject: [PATCH] Engine initialization. Some view engines (i.e. Nunjucks) rely on the manager configuration and other state in order to render templates. This change provides a convenience method to allow engines to initialize any additional state and to update the engine configuration accordingly. --- README.md | 17 ++- examples/nunjucks/index.js | 13 ++- lib/manager.js | 51 ++++++++- test/manager.js | 206 +++++++++++++++++++++++++++++++++++++ 4 files changed, 276 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0a35253..1042603 100644 --- a/README.md +++ b/README.md @@ -165,9 +165,6 @@ server.route({ method: 'GET', path: '/', handler: rootHandler }); var server = new Hapi.Server(); server.connection({ port: 8000 }); -var viewPath = Path.join(__dirname, 'templates'); -var environment = Nunjucks.configure(viewPath, { watch: false }); - var rootHandler = function (request, reply) { reply.view('index', { @@ -181,16 +178,22 @@ server.views({ html: { compile: function (src, options) { - var template = Nunjucks.compile(src, environment); + var template = Nunjucks.compile(src, options.environment); return function (context) { return template.render(context); }; + }, + + prepare: function (options, next) { + + options.compileOptions.environment = Nunjucks.configure(options.path, { watch : false }); + return next(); } } }, - path: viewPath + path: Path.join(__dirname, 'templates') }); server.route({ method: 'GET', path: '/', handler: rootHandler }); @@ -220,6 +223,10 @@ Initializes the server views manager where: where `callback` has the signature `function(err, compiled)` where `compiled` is a function with signature `function(context, options, callback)` and `callback` has the signature `function(err, rendered)`. + - `prepare(config, next)` - initializes additional engine state. + The `config` object is the engine configuration object allowing updates to be made. + This is useful for engines like Nunjucks that rely on additional state for rendering. + `next` has the signature `function(err)`. - `registerPartial(name, src)` - registers a partial for use during template rendering. The `name` is the partial path that templates should use to reference the partial and `src` is the uncompiled template string for the partial. diff --git a/examples/nunjucks/index.js b/examples/nunjucks/index.js index af57b82..e7e91bb 100644 --- a/examples/nunjucks/index.js +++ b/examples/nunjucks/index.js @@ -24,24 +24,27 @@ internals.main = function () { var server = new Hapi.Server(); server.connection({ port: 8000 }); - var viewPath = Path.join(__dirname, 'templates'); - var environment = Nunjucks.configure(viewPath, { watch: false }); - server.views({ engines: { html: { compile: function (src, options) { - var template = Nunjucks.compile(src, environment); + var template = Nunjucks.compile(src, options.environment); return function (context) { return template.render(context); }; + }, + + prepare: function (options, next) { + + options.compileOptions.environment = Nunjucks.configure(options.path, { watch: false }); + return next(); } } }, - path: viewPath + path: Path.join(__dirname, 'templates') }); server.route({ method: 'GET', path: '/', handler: rootHandler }); diff --git a/lib/manager.js b/lib/manager.js index 8277f97..efbdd4a 100755 --- a/lib/manager.js +++ b/lib/manager.js @@ -156,6 +156,9 @@ exports = module.exports = internals.Manager = function (options) { engine.cache = {}; } + // When a prepare function is provided, state needs to be initialized before trying to compile and render + engine.ready = !(engine.module.prepare && typeof engine.module.prepare === 'function'); + // Load partials and helpers self._loadPartials(engine); @@ -301,11 +304,57 @@ internals.Manager.prototype._prepare = function (template, options, callback) { return callback(Boom.badImplementation('No view engine found for file: ' + template)); } + template = template + (fileExtension ? '' : engine.suffix); + + // Engine is ready to render + + if (engine.ready) { + return this._prepareTemplates(template, engine, options, callback); + } + + // Engine needs initialization + + return this._prepareEngine(engine, function (err) { + + if (err) { + return callback(err); + } + + return self._prepareTemplates(template, engine, options, callback); + }); +}; + + +internals.Manager.prototype._prepareEngine = function (engine, next) { + + // _prepareEngine can only be invoked when the prepare function is defined + + try { + return engine.module.prepare(engine.config, function (err) { + + if (err) { + return next(err); + } + + engine.ready = true; + return next(); + }); + } + catch (err) { + return next(err); + } +}; + + +internals.Manager.prototype._prepareTemplates = function (template, engine, options, callback) { + + var self = this; + var compiled = { settings: Hoek.applyToDefaults(engine.config, options) }; - this._path(template + (fileExtension ? '' : engine.suffix), compiled.settings, false, function (err, templatePath) { + this._path(template, compiled.settings, false, function (err, templatePath) { if (err) { return callback(err); diff --git a/test/manager.js b/test/manager.js index a3f1feb..acf9830 100755 --- a/test/manager.js +++ b/test/manager.js @@ -355,6 +355,212 @@ describe('Manager', function () { }); }); + describe('with engine initialization', function () { + + it('modifies the engine options', function (done) { + + var compileOptions; + var runtimeOptions; + + var manager = new Manager({ + path: __dirname + '/templates', + engines: { + html: { + compile: function (string, options1) { + + compileOptions = options1; + + return function (context, options2) { + + runtimeOptions = options2; + return string; + }; + }, + + prepare: function (options, next) { + + options.compileOptions = { stage: 'compile' }; + options.runtimeOptions = { stage: 'render' }; + return next(); + } + } + } + }); + + manager.render('valid/test', null, null, function (err, rendered) { + + expect(err).to.not.exist(); + expect(compileOptions).to.include({ stage: 'compile' }); + expect(runtimeOptions).to.include({ stage: 'render' }); + done(); + }); + }); + + it('errors if initialization fails', function (done) { + + var manager = new Manager({ + path: __dirname + '/templates', + engines: { + html: { + compile: function (string, options1) { + + return function (context, options2) { + + return string; + }; + }, + + prepare: function (options, next) { + + return next(new Error('Initialization failed')); + } + } + } + }); + + manager.render('valid/test', null, null, function (err, rendered) { + + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.equal('Initialization failed'); + expect(rendered).to.not.exist(); + done(); + }); + }); + + it('errors if initialization throws', function (done) { + + var manager = new Manager({ + path: __dirname + '/templates', + engines: { + html: { + compile: function (string, options1) { + + return function (context, options2) { + + return string; + }; + }, + + prepare: function (options, next) { + + throw new Error('Initialization error'); + } + } + } + }); + + manager.render('valid/test', null, null, function (err, rendered) { + + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.equal('Initialization error'); + expect(rendered).to.not.exist(); + done(); + }); + }); + + it('only initializes once before rendering', function (done) { + + var initialized = 0; + + var manager = new Manager({ + path: __dirname + '/templates', + engines: { + html: { + compile: function (string, options1) { + + return function (context, options2) { + + return string; + }; + }, + + prepare: function (options, next) { + + ++initialized; + return next(); + } + } + } + }); + + expect(initialized).to.equal(0); + manager.render('valid/test', null, null, function (err, rendered1) { + + expect(err).to.not.exist(); + expect(rendered1).to.exist(); + expect(initialized).to.equal(1); + manager.render('valid/test', null, null, function (err, rendered2) { + + expect(err).to.not.exist(); + expect(rendered2).to.exist(); + expect(initialized).to.equal(1); + done(); + }); + }); + }); + + it('initializes multiple engines independently', function (done) { + + var htmlOptions; + var jadeOptions; + + var manager = new Manager({ + path: __dirname + '/templates', + engines: { + html: { + compile: function (string, options1) { + + htmlOptions = options1; + return function (context, options2) { + + return string; + }; + }, + + prepare: function (options, next) { + + options.compileOptions = { engine: 'handlebars' }; + return next(); + } + }, + + jade: { + compile: function (string, options1) { + + jadeOptions = options1; + return function (context, options2) { + + return string; + }; + }, + + prepare: function (options, next) { + + options.compileOptions = { engine: 'jade' }; + return next(); + } + } + } + }); + + manager.render('valid/test.html', null, null, function (err, rendered1) { + + expect(err).to.not.exist(); + expect(rendered1).to.exist(); + expect(htmlOptions).to.include({ engine: 'handlebars' }); + expect(jadeOptions).to.not.exist(); + + manager.render('valid/test.jade', null, null, function (err, rendered2) { + + expect(err).to.not.exist(); + expect(rendered2).to.exist(); + expect(jadeOptions).to.include({ engine: 'jade' }); + done(); + }); + }); + }); + }); + describe('with layout', function (done) { it('returns response', function (done) {