From 6c20786f93ed430fd276856e78c3b66e738097a9 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sat, 24 May 2014 17:32:10 -0700 Subject: [PATCH 01/33] Remove pack array support. Closes #1652 --- lib/composer.js | 70 +++++++++++++++++------------------------------- test/composer.js | 6 ++--- 2 files changed, 27 insertions(+), 49 deletions(-) diff --git a/lib/composer.js b/lib/composer.js index be0305c18..25c9308de 100755 --- a/lib/composer.js +++ b/lib/composer.js @@ -10,7 +10,7 @@ var Pack = require('./pack'); var internals = {}; /* -var config = [{ +var config = { pack: { cache: 'redis', app: { @@ -38,14 +38,14 @@ var config = [{ plugins: '/' } } -}]; +}; */ exports = module.exports = internals.Composer = function (manifest, options) { - this._manifest = Hoek.clone([].concat(manifest)); + this._manifest = Hoek.clone(manifest); this._settings = Hoek.clone(options || {}); - this._packs = []; + this._pack = null; }; @@ -53,50 +53,36 @@ internals.Composer.prototype.compose = function (callback) { var self = this; - // Create packs - - var sets = []; - this._manifest.forEach(function (set) { + // Create pack - Hoek.assert(set.servers && set.servers.length, 'Pack missing servers definition'); - Hoek.assert(set.plugins, 'Pack missing plugins definition'); + var set = this._manifest; - var pack = new Pack(Hoek.applyToDefaults(self._settings.pack || {}, set.pack || {})); + Hoek.assert(set.servers && set.servers.length, 'Pack missing servers definition'); + Hoek.assert(set.plugins, 'Pack missing plugins definition'); - // Load servers + this._pack = new Pack(Hoek.applyToDefaults(self._settings.pack || {}, set.pack || {})); - set.servers.forEach(function (server) { + // Load servers - if (server.host && - server.host.indexOf('$env.') === 0) { + set.servers.forEach(function (server) { - server.host = process.env[server.host.slice(5)]; - } + if (server.host && + server.host.indexOf('$env.') === 0) { - if (server.port && - typeof server.port === 'string' && - server.port.indexOf('$env.') === 0) { + server.host = process.env[server.host.slice(5)]; + } - server.port = parseInt(process.env[server.port.slice(5)], 10); - } + if (server.port && + typeof server.port === 'string' && + server.port.indexOf('$env.') === 0) { - pack.server(server.host, server.port, server.options); - }); + server.port = parseInt(process.env[server.port.slice(5)], 10); + } - sets.push({ pack: pack, plugins: set.plugins }); - self._packs.push(pack); + self._pack.server(server.host, server.port, server.options); }); - // Register plugins - - Async.forEachSeries(sets, function (set, next) { - - set.pack.require(set.plugins, next); - }, - function (err) { - - return callback(err); - }); + this._pack.require(set.plugins, callback); }; @@ -104,11 +90,7 @@ internals.Composer.prototype.start = function (callback) { callback = callback || Hoek.ignore; - Async.forEachSeries(this._packs, function (pack, next) { - - pack.start(next); - }, - function (err) { + this._pack.start(function (err) { Hoek.assert(!err, 'Failed starting plugins:', err && err.message); return callback(); @@ -125,9 +107,5 @@ internals.Composer.prototype.stop = function (options, callback) { callback = callback || Hoek.ignore; - Async.forEach(this._packs, function (pack, next) { - - pack.stop(options, next); - }, - callback); + this._pack.stop(options, callback); }; diff --git a/test/composer.js b/test/composer.js index 38dccc753..d16ff4ab8 100755 --- a/test/composer.js +++ b/test/composer.js @@ -60,7 +60,7 @@ describe('Composer', function () { expect(err).to.not.exist; composer.stop(function () { - composer._packs[0]._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { + composer._pack._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { expect(res.result).to.equal('testing123special-value'); done(); @@ -110,7 +110,7 @@ describe('Composer', function () { expect(err).to.not.exist; composer.stop(); - composer._packs[0]._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { + composer._pack._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { expect(res.result).to.equal('testing123'); done(); @@ -162,7 +162,7 @@ describe('Composer', function () { expect(err).to.not.exist; - expect(composer._packs[0].app).to.equal('only here'); + expect(composer._pack.app).to.equal('only here'); done(); }); }); From 2fcaa6a9f73d3b16ded762f0989563714bfb22ed Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 18 May 2014 06:14:48 -0700 Subject: [PATCH 02/33] Move Composer into Pack.compose(). Closes #1653 --- docs/Reference.md | 72 ++++---------- lib/cli.js | 82 +++++++--------- lib/composer.js | 111 ---------------------- lib/handler.js | 1 + lib/index.js | 1 - lib/pack.js | 74 +++++++++++++++ test/composer.js | 237 ---------------------------------------------- test/pack.js | 144 ++++++++++++++++++++++++++++ 8 files changed, 267 insertions(+), 455 deletions(-) delete mode 100755 lib/composer.js delete mode 100755 test/composer.js diff --git a/docs/Reference.md b/docs/Reference.md index 78992afaa..48fd221b2 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -68,11 +68,7 @@ - [`pack.require(name, options, callback)`](#packrequirename-options-callback) - [`pack.require(names, callback)`](#packrequirenames-callback) - [`pack.register(plugin, options, callback)`](#packregisterplugin-options-callback) -- [`Hapi.Composer`](#hapicomposer) - - [`new Composer(manifest)`](#new-composermanifest) - - [`composer.compose(callback)`](#composercomposecallback) - - [`composer.start([callback])`](#composerstartcallback) - - [`composer.stop([options], [callback])`](#composerstopoptions-callback) + - [`Pack.compose(manifest, [options], callback)`](#Packcomposemanifest-options-callback) - [Plugin interface](#plugin-interface) - [`exports.register(plugin, options, next)`](#exportsregisterplugin-options-next) - [Root methods and properties](#root-methods-and-properties) @@ -2230,21 +2226,24 @@ server.pack.register(plug, { message: 'hello' }, function (err) { }); ``` -## `Hapi.Composer` +### `Pack.compose(manifest, [options], callback)` -The `Composer` provides a simple way to construct a [`Pack`](#hapipack) from a single configuration object, including configuring servers -and registering plugins. +Provides a simple way to construct a [`Pack`](#hapipack) from a single configuration object, including configuring servers +and registering plugins where: -#### `new Composer(manifest)` - -Creates a `Composer` object instance where: - -- `manifest` - an object or array or objects where: +- `manifest` - an object with the following keys: - `pack` - the pack `options` as described in [`new Pack()`](#packserverhost-port-options). - `servers` - an array of server configuration objects where: - `host`, `port`, `options` - the same as described in [`new Server()`](#new-serverhost-port-options) with the exception that the - `cache` option is not allowed and must be configured via the pack `cache` option. The `host` and `port` keys can be set to an environment variable by prefixing the variable name with `'$env.'`. - - `plugin` - an object where each key is a plugin name, and each value is the `options` object used to register that plugin. + `cache` option is not allowed and must be configured via the pack `cache` option. The `host` and `port` keys can be set to an + environment variable by prefixing the variable name with `'$env.'`. + - `plugins` - an object where each key is a plugin name, and each value is the `options` object used to register that plugin. +- `options` - optional pack configuration used as the baseline configuration for the pack (`manifest.pack` is applied to `options`). +- `callback` - the callback method, called when all packs and servers have been created and plugins registered has the signature + `function(err, pack)` where: + - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts + (e.g. among routes, methods, state) will throw an error and will not return a callback. + - `pack` - the composed Pack object. ```javascript var Hapi = require('hapi'); @@ -2277,48 +2276,9 @@ var manifest = { } }; -var composer = new Hapi.Composer(manifest); -``` - -#### `composer.compose(callback)` - -Creates the packs described in the manifest construction where: - -- `callback` - the callback method, called when all packs and servers have been created and plugins registered has the signature - `function(err)` where: - - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts - (e.g. among routes, methods, state) will throw an error and will not return a callback. - -```javascript -composer.compose(function (err) { - - if (err) { - console.log('Failed composing'); - } -}); -``` - -#### `composer.start([callback])` - -Starts all the servers in all the pack composed where: - -- `callback` - the callback method called when all the servers have been started. +Hapi.Pack.composer(manifest, function (err, pack) { -```javascript -composer.start(function () { - - console.log('All servers started'); -}); -``` - -#### `composer.stop([options], [callback])` - -Stops all the servers in all the packs and used as described in [`server.stop([options], [callback])`](#serverstopoptions-callback). - -```javascript -pack.stop({ timeout: 60 * 1000 }, function () { - - console.log('All servers stopped'); + pack.start(); }); ``` diff --git a/lib/cli.js b/lib/cli.js index b743a5625..9b9b2de56 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -54,9 +54,8 @@ internals.loadExtras = function () { console.error('Unable to require extra file: %s (%s)', extras, err.message); process.exit(1); } +}; - return; -} internals.getManifest = function () { @@ -72,7 +71,7 @@ internals.getManifest = function () { } return manifest; -} +}; internals.loadPacks = function (manifest, callback) { @@ -89,71 +88,54 @@ internals.loadPacks = function (manifest, callback) { return callback(err); } - options.pack = { requirePath: path }; + options = { requirePath: path }; callback(null, options); }); -} - - -internals.createComposer = function (manifest, options) { - - var attached = !!internals.composer; // When composer exists events are already attached. - internals.composer = new Hapi.Composer(manifest, options); - - internals.composer.compose(function (err) { - - Hoek.assert(!err, 'Failed loading plugins: ' + (err && err.message)); - internals.composer.start(function (err) { - - Hoek.assert(!err, 'Failed starting server: ' + (err && err.message)); - - if (!attached) { - internals.attachEvents(); - } - }); - }); }; -internals.attachEvents = function () { +exports.start = function () { - process.once('SIGQUIT', internals.stop); // Use kill -s QUIT {pid} to kill the servers gracefully - process.on('SIGUSR2', internals.restart); // Use kill -s SIGUSR2 {pid} to restart the servers -}; + internals.loadExtras(); + Hapi = require('..') + var manifest = internals.getManifest(); + internals.loadPacks(manifest, function (err, options) { + if (err) { + console.error(err); + process.exit(1); + } -internals.stop = function () { + Hapi.Pack.compose(manifest, options, function (err, pack) { - internals.composer.stop(function () { + Hoek.assert(!err, 'Failed loading plugins: ' + (err && err.message)); - process.exit(0); - }); -}; + pack.start(function (err) { + Hoek.assert(!err, 'Failed starting server: ' + (err && err.message)); -internals.restart = function () { + // Use kill -s QUIT {pid} to kill the servers gracefully - console.log('Stopping...'); - internals.composer.stop(function () { + process.once('SIGQUIT', function () { - console.log('Starting...'); - internals.start(); - }); -}; + pack.stop(function () { + process.exit(0); + }); + }); -exports.start = function () { + // Use kill -s SIGUSR2 {pid} to restart the servers - internals.loadExtras(); - Hapi = require('..') - var manifest = internals.getManifest(); - internals.loadPacks(manifest, function (err, options) { + process.on('SIGUSR2', function () { - if (err) { - console.error(err); - process.exit(1); - } + console.log('Stopping...'); + pack.stop(function () { - internals.createComposer(manifest, options); + console.log('Starting...'); + internals.start(); + }); + }); + }); + }); }); }; diff --git a/lib/composer.js b/lib/composer.js deleted file mode 100755 index 25c9308de..000000000 --- a/lib/composer.js +++ /dev/null @@ -1,111 +0,0 @@ -// Load modules - -var Async = require('async'); -var Hoek = require('hoek'); -var Pack = require('./pack'); - - -// Declare internals - -var internals = {}; - -/* -var config = { - pack: { - cache: 'redis', - app: { - 'app-specific': 'value' - } - }, - servers: [ - { - port: 8001, - options: { - labels: ['api', 'nasty'] - } - }, - { - host: 'localhost', - port: '$env.PORT', - options: { - labels: ['api', 'nice'] - } - } - ], - plugins: { - furball: { - version: false, - plugins: '/' - } - } -}; -*/ - -exports = module.exports = internals.Composer = function (manifest, options) { - - this._manifest = Hoek.clone(manifest); - this._settings = Hoek.clone(options || {}); - this._pack = null; -}; - - -internals.Composer.prototype.compose = function (callback) { - - var self = this; - - // Create pack - - var set = this._manifest; - - Hoek.assert(set.servers && set.servers.length, 'Pack missing servers definition'); - Hoek.assert(set.plugins, 'Pack missing plugins definition'); - - this._pack = new Pack(Hoek.applyToDefaults(self._settings.pack || {}, set.pack || {})); - - // Load servers - - set.servers.forEach(function (server) { - - if (server.host && - server.host.indexOf('$env.') === 0) { - - server.host = process.env[server.host.slice(5)]; - } - - if (server.port && - typeof server.port === 'string' && - server.port.indexOf('$env.') === 0) { - - server.port = parseInt(process.env[server.port.slice(5)], 10); - } - - self._pack.server(server.host, server.port, server.options); - }); - - this._pack.require(set.plugins, callback); -}; - - -internals.Composer.prototype.start = function (callback) { - - callback = callback || Hoek.ignore; - - this._pack.start(function (err) { - - Hoek.assert(!err, 'Failed starting plugins:', err && err.message); - return callback(); - }); -}; - - -internals.Composer.prototype.stop = function (options, callback) { - - if (typeof options === 'function') { - callback = options; - options = {}; - } - - callback = callback || Hoek.ignore; - - this._pack.stop(options, callback); -}; diff --git a/lib/handler.js b/lib/handler.js index 8f854bbdd..7f1b633e5 100755 --- a/lib/handler.js +++ b/lib/handler.js @@ -14,6 +14,7 @@ var Methods = require('./methods'); var internals = {}; + exports.execute = function (request, next) { var finalize = function (err, result) { diff --git a/lib/index.js b/lib/index.js index 61a8ab983..8058c36ac 100755 --- a/lib/index.js +++ b/lib/index.js @@ -4,7 +4,6 @@ exports.version = require('../package.json').version; exports.error = exports.Error = exports.boom = exports.Boom = require('boom'); exports.Server = require('./server'); exports.Pack = require('./pack'); -exports.Composer = require('./composer'); exports.state = { prepareValue: require('./state').prepareValue diff --git a/lib/pack.js b/lib/pack.js index 8e3871935..880ee6db2 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -716,3 +716,77 @@ internals.Pack.prototype._invoke = function (event, callback) { return callback(err); }); }; + + +/* +var config = { + pack: { + cache: 'redis', + app: { + 'app-specific': 'value' + } + }, + servers: [ + { + port: 8001, + options: { + labels: ['api', 'nasty'] + } + }, + { + host: 'localhost', + port: '$env.PORT', + options: { + labels: ['api', 'nice'] + } + } + ], + plugins: { + furball: { + version: false, + plugins: '/' + } + } +}; +*/ + +internals.Pack.compose = function (manifest /* [options], callback */) { + + var options = arguments.length === 2 ? {} : arguments[1]; + var callback = arguments.length === 2 ? arguments[1] : arguments[2]; + + // Create pack + + Hoek.assert(manifest.servers && manifest.servers.length, 'Pack missing servers definition'); + Hoek.assert(manifest.plugins, 'Pack missing plugins definition'); + Hoek.assert(typeof callback === 'function', 'Invalid callback'); + + var pack = new internals.Pack(Hoek.applyToDefaults(options, manifest.pack || {})); + + // Load servers + + manifest.servers.forEach(function (server) { + + if (server.host && + server.host.indexOf('$env.') === 0) { + + server.host = process.env[server.host.slice(5)]; + } + + if (server.port && + typeof server.port === 'string' && + server.port.indexOf('$env.') === 0) { + + server.port = parseInt(process.env[server.port.slice(5)], 10); + } + + pack.server(server.host, server.port, server.options); + }); + + // Load plugin + + pack.require(manifest.plugins, function (err) { + + return callback(err, pack); + }); +}; diff --git a/test/composer.js b/test/composer.js deleted file mode 100755 index d16ff4ab8..000000000 --- a/test/composer.js +++ /dev/null @@ -1,237 +0,0 @@ -// Load modules - -var Lab = require('lab'); -var Hapi = require('..'); - - -// Declare internals - -var internals = {}; - - -// Test shortcuts - -var expect = Lab.expect; -var before = Lab.before; -var after = Lab.after; -var describe = Lab.experiment; -var it = Lab.test; - - -describe('Composer', function () { - - it('composes pack', function (done) { - - var manifest = { - pack: { - cache: { - engine: 'catbox-memory' - }, - app: { - my: 'special-value' - } - }, - servers: [ - { - port: 0, - options: { - labels: ['api', 'nasty', 'test'] - } - }, - { - host: 'localhost', - port: 0, - options: { - labels: ['api', 'nice'] - } - } - ], - plugins: { - '../test/pack/--test1': [{ ext: true }, {}] - } - }; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - composer.start(function (err) { - - expect(err).to.not.exist; - composer.stop(function () { - - composer._pack._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { - - expect(res.result).to.equal('testing123special-value'); - done(); - }); - }); - }); - }); - }); - - it('composes pack (env)', function (done) { - - var manifest = { - pack: { - cache: { - engine: 'catbox-memory' - } - }, - servers: [ - { - port: '$env.hapi_port', - options: { - labels: ['api', 'nasty', 'test'] - } - }, - { - host: '$env.hapi_host', - port: 0, - options: { - labels: ['api', 'nice'] - } - } - ], - plugins: { - '../test/pack/--test1': {} - } - }; - - process.env.hapi_port = '0'; - process.env.hapi_host = 'localhost'; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - composer.start(function (err) { - - expect(err).to.not.exist; - composer.stop(); - - composer._pack._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { - - expect(res.result).to.equal('testing123'); - done(); - }); - }); - }); - }); - - it('composes pack with ports', function (done) { - - var manifest = { - servers: [ - { - port: 8000 - }, - { - port: '8001', - } - ], - plugins: {} - }; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - done(); - }); - }); - - it('throws when missing servers', function (done) { - - var manifest = { - plugins: {} - }; - - var composer = new Hapi.Composer(manifest); - expect(function () { - - composer.compose(function (err) { }); - }).to.throw('Pack missing servers definition'); - done(); - }); - - it('composes pack with default pack settings', function (done) { - - var composer = new Hapi.Composer({ servers: [{}], plugins: {} }, { pack: { app: 'only here' } }); - composer.compose(function (err) { - - expect(err).to.not.exist; - - expect(composer._pack.app).to.equal('only here'); - done(); - }); - }); - - it('allows start without callback', function (done) { - - var manifest = { - servers: [ - { - port: 0, - } - ], - plugins: {} - }; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - composer.start(); - done(); - }); - }); - - it('allows stop without callback', function (done) { - - var manifest = { - servers: [ - { - port: 0, - } - ], - plugins: {} - }; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - composer.start(function () { - - composer.stop(); - done(); - }); - }); - }); - - it('throws error when start fails', function (done) { - - var manifest = { - servers: [ - { - port: 0, - } - ], - plugins: { - '../test/pack/--afterErr': {} - } - }; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - expect(function () { - - composer.start(); - }).to.throw('Failed starting plugins: Not in the mood'); - done(); - }); - }); -}); diff --git a/test/pack.js b/test/pack.js index 659bcd170..62661380f 100755 --- a/test/pack.js +++ b/test/pack.js @@ -1296,4 +1296,148 @@ describe('Pack', function () { }); }); }); + + describe('#compose', function () { + + it('composes pack', function (done) { + + var manifest = { + pack: { + cache: { + engine: 'catbox-memory' + }, + app: { + my: 'special-value' + } + }, + servers: [ + { + port: 0, + options: { + labels: ['api', 'nasty', 'test'] + } + }, + { + host: 'localhost', + port: 0, + options: { + labels: ['api', 'nice'] + } + } + ], + plugins: { + '../test/pack/--test1': [{ ext: true }, {}] + } + }; + + Hapi.Pack.compose(manifest, function (err, pack) { + + expect(err).to.not.exist; + pack.start(function (err) { + + expect(err).to.not.exist; + pack.stop(function () { + + pack._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { + + expect(res.result).to.equal('testing123special-value'); + done(); + }); + }); + }); + }); + }); + + it('composes pack (env)', function (done) { + + var manifest = { + pack: { + cache: { + engine: 'catbox-memory' + } + }, + servers: [ + { + port: '$env.hapi_port', + options: { + labels: ['api', 'nasty', 'test'] + } + }, + { + host: '$env.hapi_host', + port: 0, + options: { + labels: ['api', 'nice'] + } + } + ], + plugins: { + '../test/pack/--test1': {} + } + }; + + process.env.hapi_port = '0'; + process.env.hapi_host = 'localhost'; + + Hapi.Pack.compose(manifest, function (err, pack) { + + expect(err).to.not.exist; + pack.start(function (err) { + + expect(err).to.not.exist; + pack.stop(); + + pack._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { + + expect(res.result).to.equal('testing123'); + done(); + }); + }); + }); + }); + + it('composes pack with ports', function (done) { + + var manifest = { + servers: [ + { + port: 8000 + }, + { + port: '8001', + } + ], + plugins: {} + }; + + Hapi.Pack.compose(manifest, function (err, pack) { + + expect(err).to.not.exist; + done(); + }); + }); + + it('throws when missing servers', function (done) { + + var manifest = { + plugins: {} + }; + + expect(function () { + + Hapi.Pack.compose(manifest, function (err, pack) { }); + }).to.throw('Pack missing servers definition'); + done(); + }); + + it('composes pack with default pack settings', function (done) { + + Hapi.Pack.compose({ servers: [{}], plugins: {} }, { app: 'only here' }, function (err, pack) { + + expect(err).to.not.exist; + expect(pack.app).to.equal('only here'); + done(); + }); + }); + }); }); From 919cd31afbd9a477510c650d94f5ca93fd82e189 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 18 May 2014 07:49:07 -0700 Subject: [PATCH 03/33] Remove internal view require(). Closes #1655 --- lib/pack.js | 2 +- lib/schema.js | 11 +- lib/views.js | 43 +++---- test/ext.js | 2 +- test/pack.js | 16 +-- test/pack/--views/lib/index.js | 2 +- test/pack/--viewsLoader/index.js | 25 ---- .../node_modules/--custom/index.js | 14 --- .../node_modules/--custom/package.json | 7 -- test/pack/--viewsLoader/package.json | 7 -- test/pack/--viewsLoader/templates/test.html | 1 - test/response.js | 107 +++++------------- test/schema.js | 6 +- test/server.js | 2 +- test/views.js | 38 +++---- 15 files changed, 79 insertions(+), 204 deletions(-) delete mode 100755 test/pack/--viewsLoader/index.js delete mode 100755 test/pack/--viewsLoader/node_modules/--custom/index.js delete mode 100755 test/pack/--viewsLoader/node_modules/--custom/package.json delete mode 100755 test/pack/--viewsLoader/package.json delete mode 100755 test/pack/--viewsLoader/templates/test.html diff --git a/lib/pack.js b/lib/pack.js index 880ee6db2..c28ec917e 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -320,7 +320,7 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen Hoek.assert(!env.views, 'Cannot set plugin views manager more than once'); options.basePath = options.basePath || plugin.path; - env.views = new Views.Manager(options, root._requireFunc); + env.views = new Views.Manager(options); }; root.method = function (/* name, method, options */) { diff --git a/lib/schema.js b/lib/schema.js index 7d82afb89..bebb6ea11 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -300,12 +300,11 @@ internals['view handler'] = Joi.alternatives([ internals.view = internals.viewBase.keys({ - module: Joi.alternatives([ - Joi.object({ - compile: Joi.func().required() - }).options({ allowUnknown: true }), - Joi.string() - ]).required() + module: Joi.object({ + compile: Joi.func().required() + }) + .options({ allowUnknown: true }) + .required() }); diff --git a/lib/views.js b/lib/views.js index ff5857e44..a1ace2298 100755 --- a/lib/views.js +++ b/lib/views.js @@ -18,12 +18,10 @@ var internals = {}; // View Manager -exports.Manager = internals.Manager = function (options, requireFunc) { +exports.Manager = internals.Manager = function (options) { var self = this; - requireFunc = requireFunc || require; - var extensions = Object.keys(options.engines); Hoek.assert(extensions.length, 'Views manager requires at least one registered extension handler'); @@ -39,36 +37,27 @@ exports.Manager = internals.Manager = function (options, requireFunc) { extensions.forEach(function (extension) { var config = options.engines[extension]; - if (typeof config === 'string') { - config = { module: config }; - } + var engine = {}; - // Prevent module from being cloned + if (config.compile && + typeof config.compile === 'function') { - var module = null; - if (typeof config.module === 'object') { - module = config.module; - config.module = null; + engine.module = config; + engine.config = defaults; } + else { + Schema.assert('view', config); - config = Hoek.applyToDefaults(defaults, config); - - if (module) { - config.module = module; + engine.module = config.module; + config.module = null; // Prevent module from being cloned + engine.config = Hoek.applyToDefaults(defaults, config); + config.module = engine.module; // Restore object to original state } - Schema.assert('view', config); - - var engine = { - module: (typeof config.module === 'string' ? requireFunc(config.module) : config.module), - config: config, - suffix: '.' + extension - }; - - Hoek.assert(engine.module.compile, 'Invalid view engine module: missing compile()'); - + engine.suffix = '.' + extension; engine.compileFunc = engine.module.compile; - if (config.compileMode === 'sync') { + + if (engine.config.compileMode === 'sync') { engine.compileFunc = function (str, opt, next) { var compiled = null; @@ -96,7 +85,7 @@ exports.Manager = internals.Manager = function (options, requireFunc) { }; } - if (config.isCached) { + if (engine.config.isCached) { engine.cache = {}; } diff --git a/test/ext.js b/test/ext.js index 800794d6c..c1dd889a6 100755 --- a/test/ext.js +++ b/test/ext.js @@ -71,7 +71,7 @@ describe('Ext', function () { var server = new Hapi.Server({ views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid' } }); diff --git a/test/pack.js b/test/pack.js index 62661380f..72322e226 100755 --- a/test/pack.js +++ b/test/pack.js @@ -332,7 +332,7 @@ describe('Pack', function () { register: function (plugin, options, next) { plugin.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, basePath: __dirname + '/pack/--views', path: './templates' }); @@ -362,20 +362,6 @@ describe('Pack', function () { }); }); - it('requires plugin with views and loader', function (done) { - - var server = new Hapi.Server(); - server.pack.require({ './pack/--viewsLoader': { message: 'viewing it' } }, function (err) { - - expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/' }, function (res) { - - expect(res.result).to.equal('

{{message}}

|{"message":"viewing it"}'); - done(); - }); - }); - }); - it('requires module', function (done) { var server = new Hapi.Server(); diff --git a/test/pack/--views/lib/index.js b/test/pack/--views/lib/index.js index 1080d79ec..c3e37ad6c 100755 --- a/test/pack/--views/lib/index.js +++ b/test/pack/--views/lib/index.js @@ -8,7 +8,7 @@ var internals = {}; exports.register = function (plugin, options, next) { plugin.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: './templates' }); diff --git a/test/pack/--viewsLoader/index.js b/test/pack/--viewsLoader/index.js deleted file mode 100755 index 995f4bd68..000000000 --- a/test/pack/--viewsLoader/index.js +++ /dev/null @@ -1,25 +0,0 @@ -// Declare internals - -var internals = {}; - - -// Plugin registration - -exports.register = function (plugin, options, next) { - - plugin.loader(require); - - plugin.views({ - engines: { - 'html': { - compileMode: 'async', - module: '--custom' - } - }, - path: './templates' - }); - - plugin.route({ path: '/', method: 'GET', handler: function (request, reply) { return reply.view('test', { message: options.message }); } }); - - return next(); -}; diff --git a/test/pack/--viewsLoader/node_modules/--custom/index.js b/test/pack/--viewsLoader/node_modules/--custom/index.js deleted file mode 100755 index 2c3af7b7e..000000000 --- a/test/pack/--viewsLoader/node_modules/--custom/index.js +++ /dev/null @@ -1,14 +0,0 @@ -// Declare internals - -var internals = {}; - - -exports.compile = function (string, options, callback) { - - var renderer = function (context, opt, next) { - - return next(null, string + '|' + JSON.stringify(context)); - }; - - return callback(null, renderer); -} diff --git a/test/pack/--viewsLoader/node_modules/--custom/package.json b/test/pack/--viewsLoader/node_modules/--custom/package.json deleted file mode 100755 index a332d8cc8..000000000 --- a/test/pack/--viewsLoader/node_modules/--custom/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "--custom", - "description": "Test plugin module", - "version": "0.0.1", - "private": true, - "main": "index" -} diff --git a/test/pack/--viewsLoader/package.json b/test/pack/--viewsLoader/package.json deleted file mode 100755 index cc883b23c..000000000 --- a/test/pack/--viewsLoader/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "--views", - "description": "Test plugin module", - "version": "0.0.1", - "private": true, - "main": "./index" -} diff --git a/test/pack/--viewsLoader/templates/test.html b/test/pack/--viewsLoader/templates/test.html deleted file mode 100755 index 73d402995..000000000 --- a/test/pack/--viewsLoader/templates/test.html +++ /dev/null @@ -1 +0,0 @@ -

{{message}}

\ No newline at end of file diff --git a/test/response.js b/test/response.js index b9b35fcd1..7b311053f 100755 --- a/test/response.js +++ b/test/response.js @@ -1147,7 +1147,7 @@ describe('Response', function () { var options = { debug: false, views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname } }; @@ -2488,7 +2488,7 @@ describe('Response', function () { views: { engines: { html: { - module: 'handlebars', + module: require('handlebars'), path: __dirname + '/templates/valid' } } @@ -2516,7 +2516,7 @@ describe('Response', function () { views: { engines: { html: { - module: 'handlebars', + module: require('handlebars'), path: __dirname + '/templates/valid', contentType: 'something/else' } @@ -2539,57 +2539,12 @@ describe('Response', function () { }); }); - it('should throw if view module not found', function (done) { - - var fn = function () { - - var failServer = new Hapi.Server({ - views: { - path: __dirname + '/templates/valid', - engines: { - 'html': 'handlebars', - 'jade': 'jade', - 'hbar': { - module: require('handlebars'), - compile: function (engine) { return engine.compile; } - }, - 'err': { - module: 'hapi-module-that-does-not-exist' - } - } - } - }); - }; - expect(fn).to.throw(); - done(); - }); - - it('should work if view engine module is a pre-required module', function (done) { - - var options = { - views: { - path: __dirname + '/templates/valid', - engines: { - 'test': { - module: require('jade') - } - } - } - }; - var fn = function () { - - var passServer = new Hapi.Server(options); - }; - expect(fn).to.not.throw(); - done(); - }); - it('returns error on invalid template path', function (done) { var server = new Hapi.Server({ debug: false, views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/invalid' } }); @@ -2616,7 +2571,7 @@ describe('Response', function () { var server = new Hapi.Server({ views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid' } }); @@ -2642,7 +2597,7 @@ describe('Response', function () { var server = new Hapi.Server({ debug: false, views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid' } }); @@ -2667,7 +2622,7 @@ describe('Response', function () { var server = new Hapi.Server({ debug: false, views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid' } }); @@ -2692,7 +2647,7 @@ describe('Response', function () { var server = new Hapi.Server({ debug: false, views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, allowInsecureAccess: true, path: __dirname + '/templates/valid/helpers' } @@ -2719,7 +2674,7 @@ describe('Response', function () { var server = new Hapi.Server({ debug: false, views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid' } }); @@ -2744,7 +2699,7 @@ describe('Response', function () { var server = new Hapi.Server({ debug: false, views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid' } }); @@ -2770,7 +2725,7 @@ describe('Response', function () { var layoutServer = new Hapi.Server(); layoutServer.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates', layout: true }); @@ -2795,7 +2750,7 @@ describe('Response', function () { var layoutServer = new Hapi.Server(); layoutServer.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, basePath: '/none/shall/pass', path: __dirname + '/templates', layout: true @@ -2821,7 +2776,7 @@ describe('Response', function () { var layoutServer = new Hapi.Server(); layoutServer.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates', layout: true }); @@ -2846,7 +2801,7 @@ describe('Response', function () { var layoutServer = new Hapi.Server(); layoutServer.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates', layout: 'otherLayout' }); @@ -2871,7 +2826,7 @@ describe('Response', function () { var layoutServer = new Hapi.Server(); layoutServer.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, basePath: __dirname, path: 'templates', layoutPath: 'templates/layout', @@ -2898,7 +2853,7 @@ describe('Response', function () { var layoutServer = new Hapi.Server({ debug: false }); layoutServer.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates', layout: 'missingLayout' }); @@ -2921,7 +2876,7 @@ describe('Response', function () { var layoutServer = new Hapi.Server({ debug: false }); layoutServer.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates', layout: 'invalidLayout' }); @@ -2944,7 +2899,7 @@ describe('Response', function () { var layoutServer = new Hapi.Server(); layoutServer.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates', layout: true }); @@ -2969,7 +2924,7 @@ describe('Response', function () { var layoutServer = new Hapi.Server({ debug: false }); layoutServer.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid', layout: true }); @@ -2993,7 +2948,7 @@ describe('Response', function () { var layoutServer = new Hapi.Server({ debug: false }); layoutServer.views({ - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid', layout: true }); @@ -3022,7 +2977,7 @@ describe('Response', function () { var testServer = new Hapi.Server({ views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid' } }); @@ -3053,7 +3008,7 @@ describe('Response', function () { var testServer = new Hapi.Server({ views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid', isCached: true } @@ -3082,7 +3037,7 @@ describe('Response', function () { var testServer = new Hapi.Server({ views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid', isCached: false } @@ -3112,8 +3067,8 @@ describe('Response', function () { views: { path: __dirname + '/templates/valid', engines: { - 'html': 'handlebars', - 'jade': 'jade', + 'html': require('handlebars'), + 'jade': require('jade'), 'hbar': { module: { compile: function (engine) { return engine.compile; } @@ -3145,8 +3100,8 @@ describe('Response', function () { views: { path: __dirname + '/templates/valid', engines: { - 'html': 'handlebars', - 'jade': 'jade', + 'html': require('handlebars'), + 'jade': require('jade'), 'hbar': { module: { compile: function (engine) { return engine.compile; } @@ -3178,8 +3133,8 @@ describe('Response', function () { views: { path: __dirname + '/templates/valid', engines: { - 'html': 'handlebars', - 'jade': 'jade', + 'html': require('handlebars'), + 'jade': require('jade'), 'hbar': { module: { compile: function (engine) { return engine.compile; } @@ -3210,8 +3165,8 @@ describe('Response', function () { views: { path: __dirname + '/templates/valid', engines: { - 'html': 'handlebars', - 'jade': 'jade', + 'html': require('handlebars'), + 'jade': require('jade'), 'hbar': { module: { compile: function (engine) { return engine.compile; } diff --git a/test/schema.js b/test/schema.js index 46b29d81d..b3cab634b 100755 --- a/test/schema.js +++ b/test/schema.js @@ -182,9 +182,9 @@ describe('Schema', function () { expect(function () { var config = { - module: "baz", - path: "foo", - defaultExtension: "bar" + module: require('handlebars'), + path: 'foo', + defaultExtension: 'bar' }; Schema.assert('view', config); }).to.not.throw(); diff --git a/test/server.js b/test/server.js index 3334e9bbd..31a0d3980 100755 --- a/test/server.js +++ b/test/server.js @@ -633,7 +633,7 @@ describe('Server', function () { var server = new Hapi.Server({ views: { - engines: { 'html': 'handlebars' } + engines: { 'html': require('handlebars') } } }); expect(server._views).to.exist; diff --git a/test/views.js b/test/views.js index bb885d8f3..b9b0dc5b5 100755 --- a/test/views.js +++ b/test/views.js @@ -83,7 +83,7 @@ describe('Views', function () { it('allows valid (no layouts)', function (done) { var testView = new Views.Manager({ - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates', layout: false }); @@ -99,7 +99,7 @@ describe('Views', function () { it('renders without context', function (done) { var testView = new Views.Manager({ - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates' }); @@ -115,7 +115,7 @@ describe('Views', function () { var testView = new Views.Manager({ defaultExtension: 'html', - engines: { html: 'handlebars', jade: 'jade' }, + engines: { html: require('handlebars'), jade: require('jade') }, path: __dirname + '/templates' }); @@ -130,7 +130,7 @@ describe('Views', function () { it('allows relative path with no base', function (done) { var testView = new Views.Manager({ - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: './test/templates', layout: false }); @@ -146,7 +146,7 @@ describe('Views', function () { it('allows valid (with layouts)', function (done) { var testViewWithLayouts = new Views.Manager({ - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates', layout: true }); @@ -162,7 +162,7 @@ describe('Views', function () { it('allows absolute path', function (done) { var testViewWithLayouts = new Views.Manager({ - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates', layout: __dirname + '/templates/layout', allowAbsolutePaths: true @@ -179,7 +179,7 @@ describe('Views', function () { it('errors on invalid layout', function (done) { var views = new Views.Manager({ - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates', layout: 'badlayout' }); @@ -195,7 +195,7 @@ describe('Views', function () { it('errors on invalid layout path', function (done) { var views = new Views.Manager({ - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates', layout: '/badlayout' }); @@ -211,7 +211,7 @@ describe('Views', function () { it('allows valid jade layouts', function (done) { var testViewWithJadeLayouts = new Views.Manager({ - engines: { 'jade': 'jade' }, + engines: { 'jade': require('jade') }, path: __dirname + '/templates' + '/valid/', layout: true }); @@ -226,7 +226,7 @@ describe('Views', function () { it('should work and not throw without jade layouts', function (done) { var testViewWithoutJadeLayouts = new Views.Manager({ - engines: { 'jade': 'jade' }, + engines: { 'jade': require('jade') }, path: __dirname + '/templates' + '/valid/', layout: false }); @@ -240,7 +240,7 @@ describe('Views', function () { it('allows basePath, template name, and no path', function (done) { - var views = new Views.Manager({ engines: { html: 'handlebars' } }); + var views = new Views.Manager({ engines: { html: require('handlebars') } }); views.render('test', { title: 'test', message: 'Hapi' }, { basePath: __dirname + '/templates/valid' }, function (err, rendered, config) { expect(rendered).to.exist; @@ -252,7 +252,7 @@ describe('Views', function () { it('errors when referencing non existant partial (with layouts)', function (done) { var testViewWithLayouts = new Views.Manager({ - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates', layout: true }); @@ -267,7 +267,7 @@ describe('Views', function () { it('errors when referencing non existant partial (no layouts)', function (done) { var testView = new Views.Manager({ - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates', layout: false }); @@ -283,7 +283,7 @@ describe('Views', function () { it('errors if context uses layoutKeyword as a key', function (done) { var testViewWithLayouts = new Views.Manager({ - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates', layout: true }); @@ -299,7 +299,7 @@ describe('Views', function () { it('errors on compile error (invalid template code)', function (done) { var testView = new Views.Manager({ - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates', layout: false }); @@ -378,7 +378,7 @@ describe('Views', function () { path: __dirname + '/templates/valid', partialsPath: __dirname + '/templates/valid/partials', helpersPath: __dirname + '/templates/valid/helpers', - engines: { html: 'jade' } + engines: { html: require('jade') } }); tempView.render('testPartials', {}, null, function (err, rendered, config) { @@ -457,7 +457,7 @@ describe('Views', function () { var options = { views: { - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates' } }; @@ -479,7 +479,7 @@ describe('Views', function () { var options = { views: { - engines: { 'jade': 'jade' }, + engines: { 'jade': require('jade') }, path: __dirname + '/templates' } }; @@ -503,7 +503,7 @@ describe('Views', function () { var options = { views: { - engines: { html: 'handlebars' }, + engines: { html: require('handlebars') }, path: __dirname + '/templates' } }; From 0e312e628fb7dce039f595d8d569f211bf0f8301 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 18 May 2014 08:04:07 -0700 Subject: [PATCH 04/33] #1655 --- README.md | 2 +- docs/README.md | 3 ++- docs/Reference.md | 6 +++--- package.json | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 60a2f6f8d..d16d8fe58 100755 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ infrastructure. The framework supports a powerful plugin architecture for pain-f For the latest updates and release information follow [@hapijs](https://twitter.com/hapijs) on twitter. -Current version: **5.1.x** ([release notes](https://github.com/spumko/hapi/issues?labels=release+notes&page=1&state=closed)) +Current version: **6.0.x** ([release notes](https://github.com/spumko/hapi/issues?labels=release+notes&page=1&state=closed)) [![Build Status](https://secure.travis-ci.org/spumko/hapi.png)](http://travis-ci.org/spumko/hapi) diff --git a/docs/README.md b/docs/README.md index fdf6a78aa..114208c0a 100755 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,8 @@ ## Current Documentation - [v5.1.x](https://github.com/spumko/hapi/blob/master/docs/Reference.md) + [v6.0.x](https://github.com/spumko/hapi/blob/master/docs/Reference.md) ## Previous Documentation + [v5.1.x](https://github.com/spumko/hapi/blob/v5.1.0/docs/Reference.md) [v5.0.x](https://github.com/spumko/hapi/blob/v5.0.0/docs/Reference.md) [v4.1.x](https://github.com/spumko/hapi/blob/v4.1.0/docs/Reference.md) [v4.0.x](https://github.com/spumko/hapi/blob/v4.0.0/docs/Reference.md) diff --git a/docs/Reference.md b/docs/Reference.md index 48fd221b2..7624e1b4c 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -1,4 +1,4 @@ -# 5.1.x API Reference +# 6.0.x API Reference - [`Hapi.Server`](#hapiserver) - [`new Server([host], [port], [options])`](#new-serverhost-port-options) @@ -264,9 +264,9 @@ When creating a server instance, the following options configure the server's be - `views` - enables support for view rendering (using templates to generate responses). Disabled by default. To enable, set to an object with the following options: - - `engines` - (required) an object where each key is a file extension (e.g. 'html', 'jade'), mapped to the npm module name (string) used for + - `engines` - (required) an object where each key is a file extension (e.g. 'html', 'jade'), mapped to the npm module used for rendering the templates. Alternatively, the extension can be mapped to an object with the following options: - - `module` - the npm module name (string) to require or an object with: + - `module` - the npm module used for rendering the templates. The module object must contain: - `compile()` - the rendering function. The required function signature depends on the `compileMode` settings. If the `compileMode` is `'sync'`, the signature is `compile(template, options)`, the return value is a function with signature `function(context, options)`, and the method is allowed to throw errors. If the `compileMode` is `'async'`, the signature is `compile(template, options, callback)` diff --git a/package.json b/package.json index 0a3434f88..b217859a4 100755 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "hapi", "description": "HTTP Server framework", "homepage": "http://hapijs.com", - "version": "5.1.0", + "version": "6.0.0", "repository": { "type": "git", "url": "git://github.com/spumko/hapi" From b4183c5ee4dc5292799471801b9eef38621eb4e6 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 25 May 2014 16:06:38 -0700 Subject: [PATCH 05/33] Remove pack.require(). Closes #1656 --- examples/auth.js | 2 +- lib/cli.js | 3 +- lib/pack.js | 299 ++++------- package.json | 1 - test/cli.js | 16 +- test/pack.js | 482 ++++++++---------- test/pack/--afterErr/index.js | 5 + test/pack/--auth/index.js | 5 + test/pack/--child/lib/index.js | 7 +- test/pack/--context/lib/index.js | 5 + test/pack/--deps1/index.js | 5 + test/pack/--deps2/index.js | 5 + test/pack/--deps3/index.js | 5 + test/pack/--handler/index.js | 9 +- test/pack/--loaded/lib/index.js | 5 + test/pack/--loader/lib/index.js | 13 - .../node_modules/--inner/lib/index.js | 13 - .../node_modules/--inner/package.json | 7 - test/pack/--loader/package.json | 7 - test/pack/--skip/lib/index.js | 5 + test/pack/--test1/lib/index.js | 6 + test/pack/--test2/lib/index.js | 6 +- test/pack/--views/lib/index.js | 12 + 23 files changed, 413 insertions(+), 510 deletions(-) mode change 100644 => 100755 test/pack/--loaded/lib/index.js delete mode 100755 test/pack/--loader/lib/index.js delete mode 100755 test/pack/--loader/node_modules/--inner/lib/index.js delete mode 100755 test/pack/--loader/node_modules/--inner/package.json delete mode 100755 test/pack/--loader/package.json diff --git a/examples/auth.js b/examples/auth.js index 5838d32f5..770a1be4f 100755 --- a/examples/auth.js +++ b/examples/auth.js @@ -67,7 +67,7 @@ internals.handler = function (request, reply) { internals.main = function () { var server = new Hapi.Server(8000); - server.pack.require(['hapi-auth-basic', 'hapi-auth-hawk'], function (err) { + server.pack.register([require('hapi-auth-basic'), require('hapi-auth-hawk')], function (err) { server.auth.strategy('hawk', 'hawk', { getCredentialsFunc: internals.getCredentials }); server.auth.strategy('basic', 'basic', { validateFunc: internals.validate }); diff --git a/lib/cli.js b/lib/cli.js index 9b9b2de56..35f87805e 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -88,7 +88,7 @@ internals.loadPacks = function (manifest, callback) { return callback(err); } - options = { requirePath: path }; + options = { relativeTo: path }; callback(null, options); }); }; @@ -99,6 +99,7 @@ exports.start = function () { internals.loadExtras(); Hapi = require('..') var manifest = internals.getManifest(); + internals.loadPacks(manifest, function (err, options) { if (err) { diff --git a/lib/pack.js b/lib/pack.js index c28ec917e..0844add16 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -11,7 +11,6 @@ var Defaults = require('./defaults'); var Ext = require('./ext'); var Methods = require('./methods'); var Handler = require('./handler'); -var Hapi = require('./index'); var Utils = require('./utils'); @@ -25,11 +24,6 @@ exports = module.exports = internals.Pack = function (options) { options = options || {}; this._settings = {}; - - if (options.requirePath) { - this._settings.requirePath = Path.resolve(options.requirePath); - } - this._settings.debug = Hoek.applyToDefaults(Defaults.server.debug, options.debug); this._servers = []; // List of all pack server members @@ -45,8 +39,6 @@ exports = module.exports = internals.Pack = function (options) { this.events = new Events.EventEmitter(); // Consolidated subscription to all servers' events this.app = options.app || {}; - this.hapi = require('./'); - if (options.cache) { var caches = Array.isArray(options.cache) ? options.cache : [options.cache]; for (var i = 0, il = caches.length; i < il; ++i) { @@ -169,39 +161,91 @@ internals.Pack.prototype.log = function (tags, data, timestamp, _server) { }; -internals.Pack.prototype.register = function (plugin/*, [options], callback */) { +internals.Pack.prototype.register = function (plugins /*, [options], callback */) { - // Validate arguments + var self = this; + plugins = [].concat(plugins); var options = (arguments.length === 3 ? arguments[1] : null); var callback = (arguments.length === 3 ? arguments[2] : arguments[1]); - this._register(plugin, options, callback, null); + /* + var register = function (plugin, options, next) { next(); } + register.attributes = { + name: 'plugin', + version: '1.1.1', + pkg: require('../package.json') + }; + + plugin = { + register: register, // plugin: { register } when assigned a directly required module + name: 'plugin', // || register.attributes.name || register.attributes.pkg.name + version: '1.1.1', // -optional- || register.attributes.version || register.attributes.pkg.version + options: {} // -optional- + }; + */ + + var registrations = []; + for (var i = 0, il = plugins.length; i < il; ++i) { + var plugin = plugins[i]; + var hint = (plugins.length > 1 ? '(' + i + ')' : ''); + + Hoek.assert(typeof plugin === 'object', 'Invalid plugin object', hint); + Hoek.assert(!!plugin.register ^ !!plugin.plugin, 'One of plugin or register required but cannot include both', hint); + Hoek.assert(typeof plugin.register === 'function' || (typeof plugin.plugin === 'object' && typeof plugin.plugin.register === 'function'), 'Plugin register must be a function or a required plugin module', hint); + + var register = plugin.register || plugin.plugin.register; + var attributes = register.attributes || {}; + + Hoek.assert(plugin.name || attributes, 'Incompatible plugin missing register function attributes', hint); + Hoek.assert(plugin.name || attributes.name || (attributes.pkg && attributes.pkg.name), 'Missing plugin name', hint); + + var item = { + register: register, + name: plugin.name || attributes.name || attributes.pkg.name, + version: plugin.version || attributes.version || (attributes.pkg && attributes.pkg.version) || '0.0.0', + options: plugin.options + }; + + registrations.push(item); + } + + var dependencies = {}; + Async.forEachSeries(registrations, function (item, next) { + + self._register(item, {}, dependencies, next); + }, + function (err) { + + if (err) { + return callback(err); + } + + Object.keys(dependencies).forEach(function (deps) { + + dependencies[deps].forEach(function (dep) { + + Hoek.assert(self._env[dep], 'Plugin', deps, 'missing dependencies:', dep); + }); + }); + + return callback(); + }); }; -internals.Pack.prototype._register = function (plugin, options, callback, _dependencies) { +internals.Pack.prototype._register = function (plugin, options, dependencies, callback) { var self = this; - // Validate arguments - - Hoek.assert(plugin, 'Missing plugin'); - Hoek.assert(callback, 'Missing callback'); Hoek.assert(!this._env[plugin.name], 'Plugin already registered:', plugin.name); - Hoek.assert(plugin.name, 'Plugin missing name'); - Hoek.assert(plugin.name !== '?', 'Plugin name cannot be \'?\''); - Hoek.assert(plugin.version, 'Plugin missing version'); - Hoek.assert(plugin.register && typeof plugin.register === 'function', 'Plugin missing register() method'); - - var dependencies = _dependencies || {}; // Setup environment var env = { name: plugin.name, - path: plugin.path, - bind: null + bind: null, + path: null }; this._env[plugin.name] = env; @@ -261,10 +305,9 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen var root = step(); - root.version = Hapi.version; root.hapi = self.hapi; + root.version = plugin.version; root.app = self.app; - root.path = plugin.path; root.plugins = self.plugins; root.events = self.events; root.methods = self._methods.methods; @@ -281,6 +324,11 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen self.log(tags, data, timestamp); }; + root.register = function () { + + internals.Pack.prototype.register.apply(self, arguments); + }; + root.dependency = function (deps, after) { Hoek.assert(!after || typeof after === 'function', 'Invalid after method'); @@ -304,22 +352,22 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen self._ext.add('onPreStart', method, {}, env); }; - root.loader = function (requireFunc) { - - Hoek.assert(!requireFunc || typeof requireFunc === 'function', 'Argument must be null or valid function'); - root._requireFunc = requireFunc; - }; - root.bind = function (bind) { Hoek.assert(typeof bind === 'object', 'bind must be an object'); env.bind = bind; }; + root.path = function (path) { + + Hoek.assert(path && typeof path === 'string', 'path must be a non-empty string'); + env.path = path; + } + root.views = function (options) { Hoek.assert(!env.views, 'Cannot set plugin views manager more than once'); - options.basePath = options.basePath || plugin.path; + options.basePath = options.basePath || env.path; env.views = new Views.Manager(options); }; @@ -345,28 +393,11 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen return self._provisionCache(options, 'plugin', plugin.name, options.segment); }; - root.require = function (name/*, [options], requireCallback*/) { - - var options = (arguments.length === 3 ? arguments[1] : null); - var requireCallback = (arguments.length === 3 ? arguments[2] : arguments[1]); - - self._require(name, options, requireCallback, root._requireFunc); - }; - env.root = root; // Register - plugin.register.call(null, root, options || {}, function (err) { - - if (!_dependencies && - dependencies[plugin.name]) { - - Hoek.assert(!dependencies[plugin.name].length, 'Plugin', plugin.name, 'missing dependencies:', dependencies[plugin.name].join(', ')); - } - - callback(err); - }); + plugin.register.call(null, root, plugin.options, callback); }; @@ -428,147 +459,6 @@ internals.Pack.prototype._select = function (labels, subset) { }; -/* - name: - 'plugin' - module in main process node_module directory - './plugin' - relative path to file where require is called - '/plugin' - absolute path - { 'plugin': { plugin-options } } - object where keys are loaded as module names (above) and values are plugin options - [ 'plugin' ] - array of plugin names, without plugin options -*/ - -internals.Pack.prototype.require = function (name/*, [options], callback*/) { - - var options = (arguments.length === 3 ? arguments[1] : null); - var callback = (arguments.length === 3 ? arguments[2] : arguments[1]); - - this._require(name, options, callback); -}; - - -internals.Pack.prototype._require = function (name, options, callback, requireFunc) { - - var self = this; - - Hoek.assert(name && (typeof name === 'string' || typeof name === 'object'), 'Invalid plugin name(s) object: must be string, object, or array'); - Hoek.assert(!options || typeof name === 'string', 'Cannot provide options in a multi-plugin operation'); - - requireFunc = requireFunc || require; - - var callerPath = internals.getSourceFilePath(); // Must be called outside any other function to keep call stack size identical - - var parse = function () { - - var registrations = []; - - if (typeof name === 'string') { - registrations.push({ name: name, options: options }); - } - else if (Array.isArray(name)) { - name.forEach(function (item) { - - registrations.push({ name: item, options: null }); - }); - } - else { - Object.keys(name).forEach(function (item) { - - registrations.push({ name: item, options: name[item] }); - }); - } - - var dependencies = {}; - Async.forEachSeries(registrations, function (item, next) { - - load(item, dependencies, next); - }, - function (err) { - - Object.keys(dependencies).forEach(function (deps) { - - dependencies[deps].forEach(function (dep) { - - Hoek.assert(self._env[dep], 'Plugin', deps, 'missing dependencies:', dep); - }); - }); - - return callback(err); - }); - }; - - var load = function (item, dependencies, next) { - - var itemName = item.name; - if (itemName[0] === '.') { - itemName = Path.join(callerPath, itemName); - } - else if (itemName[0] !== '/' && - self._settings.requirePath) { - - itemName = Path.join(self._settings.requirePath, itemName); - } - - var packageFile = Path.join(itemName, 'package.json'); - - var mod = requireFunc(itemName); // Will throw if require fails - var pkg = requireFunc(packageFile); - - var plugin = { - name: pkg.name, - version: pkg.version, - register: mod.register, - path: internals.packagePath(pkg.name, packageFile) - }; - - self._register(plugin, item.options, next, dependencies); - }; - - parse(); -}; - - -internals.getSourceFilePath = function () { - - var stack = Hoek.callStack(); - var callerFile = ''; - var i = 0; - var il = stack.length; - - while (callerFile === '' && i < il) { - var stackLine = stack[i]; - if (stackLine[3].lastIndexOf('.require') === stackLine[3].length - 8) { // The file that calls require is next - callerFile = stack[i + 1][0]; - } - ++i; - } - - return Path.dirname(callerFile); -}; - - -internals.packagePath = function (name, packageFile) { - - var path = null; - - var keys = Object.keys(require.cache); - var i = 0; - var il = keys.length; - - while (path === null && i < il) { - var key = keys[i]; - if (key.indexOf(packageFile) === key.length - packageFile.length) { - var record = require.cache[key]; - if (record.exports.name === name) { - path = Path.dirname(key); - } - } - ++i; - } - - return path; -}; - - internals.Pack.prototype.start = function (callback) { var self = this; @@ -750,18 +640,19 @@ var config = { }; */ -internals.Pack.compose = function (manifest /* [options], callback */) { +internals.Pack.compose = function (manifest /*, [options], callback */) { var options = arguments.length === 2 ? {} : arguments[1]; var callback = arguments.length === 2 ? arguments[1] : arguments[2]; // Create pack - Hoek.assert(manifest.servers && manifest.servers.length, 'Pack missing servers definition'); - Hoek.assert(manifest.plugins, 'Pack missing plugins definition'); + Hoek.assert(manifest.servers && manifest.servers.length, 'Manifest missing servers definition'); + Hoek.assert(manifest.plugins, 'Manifest missing plugins definition'); + Hoek.assert(options, 'Invalid options'); Hoek.assert(typeof callback === 'function', 'Invalid callback'); - var pack = new internals.Pack(Hoek.applyToDefaults(options, manifest.pack || {})); + var pack = new internals.Pack(manifest.pack); // Load servers @@ -785,7 +676,21 @@ internals.Pack.compose = function (manifest /* [options], callback */) { // Load plugin - pack.require(manifest.plugins, function (err) { + var plugins = []; + var names = Object.keys(manifest.plugins); + names.forEach(function (name) { + + var path = name; + if (options.relativeTo && + path[0] === '.') { + + path = Path.join(options.relativeTo, path); + } + + plugins.push({ plugin: require(path), options: manifest.plugins[name] }); + }); + + pack.register(plugins, function (err) { return callback(err, pack); }); diff --git a/package.json b/package.json index b217859a4..9401881f0 100755 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "lab": "3.x.x", "handlebars": "1.2.x", "jade": "1.0.x", - "hapi-plugin-test": "2.x.x", "form-data": "0.1.x" }, "bin": { diff --git a/test/cli.js b/test/cli.js index 8a8282b4a..3d74bda2f 100755 --- a/test/cli.js +++ b/test/cli.js @@ -60,7 +60,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; @@ -114,7 +114,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; @@ -172,7 +172,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; @@ -200,7 +200,7 @@ describe('Hapi command line', function () { }); }); - it('errors when it can\'t require the extra module', function (done) { + it('errors when it cannot require the extra module', function (done) { var manifest = { pack: { @@ -227,7 +227,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; @@ -256,7 +256,7 @@ describe('Hapi command line', function () { done(); }); }); - it('errors when it can\'t require the extra module from absolute path', function (done) { + it('errors when it cannot require the extra module from absolute path', function (done) { var manifest = { pack: { @@ -283,7 +283,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; @@ -340,7 +340,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; diff --git a/test/pack.js b/test/pack.js index 72322e226..ecf187f5c 100755 --- a/test/pack.js +++ b/test/pack.js @@ -46,7 +46,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '5.0.0', register: function (plugin, options, next) { var a = plugin.select('a'); @@ -95,9 +94,6 @@ describe('Pack', function () { expect(routesList(pack._servers[1])).to.deep.equal(['/a', '/all', '/sodd']); expect(routesList(pack._servers[2])).to.deep.equal(['/a', '/ab', '/all']); expect(routesList(pack._servers[3])).to.deep.equal(['/all', '/sodd', '/memoryx']); - - expect(pack._servers[0].pack.list.test.version).to.equal('5.0.0'); - done(); }); }); @@ -109,15 +105,15 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '5.0.0', register: function (plugin, options, next) { expect(options.something).to.be.true; next(); - } + }, + options: { something: true } }; - pack.register(plugin, { something: true }, function (err) { + pack.register(plugin, function (err) { expect(err).to.not.exist; done(); @@ -128,34 +124,52 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '2.0.0', register: function (plugin, options, next) { expect(options.something).to.be.true; next(); - } + }, + options: { something: true } }; var server = new Hapi.Server(); - server.pack.register(plugin, { something: true }, function (err) { + server.pack.register(plugin, function (err) { expect(err).to.not.exist; done(); }); }); - it('throws when plugin missing register', function (done) { + it('returns plugin error', function (done) { var plugin = { name: 'test', - version: '2.0.0' + register: function (plugin, options, next) { + + next(new Error('from plugin')); + } + }; + + var server = new Hapi.Server(); + server.pack.register(plugin, function (err) { + + expect(err).to.exist; + expect(err.message).to.equal('from plugin'); + done(); + }); + }); + + it('throws when plugin missing register', function (done) { + + var plugin = { + name: 'test' }; var server = new Hapi.Server(); expect(function () { server.pack.register(plugin, { something: true }, function (err) { }); - }).to.throw('Plugin missing register() method'); + }).to.throw('One of plugin or register required but cannot include both'); done(); }); @@ -169,31 +183,107 @@ describe('Pack', function () { done(); }); - it('requires plugin', function (done) { + it('throws when plugin contains non object plugin property', function (done) { - var pack = new Hapi.Pack(); - pack.server({ labels: ['s1', 'a', 'b'] }); - pack.server({ labels: ['s2', 'a', 'test'] }); - pack.server({ labels: ['s3', 'a', 'b', 'd', 'cache'] }); - pack.server({ labels: ['s4', 'b', 'test', 'cache'] }); + var plugin = { + name: 'test', + plugin: 5 + }; - pack.require('./pack/--test1', {}, function (err) { + var server = new Hapi.Server(); + expect(function () { - expect(err).to.not.exist; + server.pack.register(plugin, function (err) { }); + }).to.throw('Plugin register must be a function or a required plugin module'); + done(); + }); - expect(pack._servers[0]._router.routes['get']).to.not.exist; - expect(routesList(pack._servers[1])).to.deep.equal(['/test1']); - expect(pack._servers[2]._router.routes['get']).to.not.exist; - expect(routesList(pack._servers[3])).to.deep.equal(['/test1']); + it('throws when plugin contains an object plugin property with invalid register', function (done) { - expect(pack._servers[0].plugins['--test1'].add(1, 3)).to.equal(4); - expect(pack._servers[0].plugins['--test1'].glue('1', '3')).to.equal('13'); + var plugin = { + name: 'test', + plugin: { + register: 5 + } + }; - done(); + var server = new Hapi.Server(); + expect(function () { + + server.pack.register(plugin, function (err) { }); + }).to.throw('Plugin register must be a function or a required plugin module'); + done(); + }); + + it('throws when plugin contains a pkg attribute without a name', function (done) { + + var plugin = { + register: function () { } + }; + + plugin.register.attributes = { + pkg: { + + } + }; + + var server = new Hapi.Server(); + expect(function () { + + server.pack.register(plugin, function (err) { }); + }).to.throw('Missing plugin name'); + done(); + }); + + it('sets version to 0.0.0 if missing', function (done) { + + var plugin = { + register: function (plugin, options, next) { + + plugin.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(plugin.version); } }); + next(); + } + }; + + plugin.register.attributes = { + pkg: { + name: '--steve' + } + }; + + var server = new Hapi.Server(); + + server.pack.register(plugin, function (err) { + + expect(err).to.not.exist; + server.inject('/', function (res) { + + expect(res.result).to.equal('0.0.0'); + done(); + }); }); }); - it('requires plugin with absolute path', function (done) { + it('throws when plugin sets undefined path', function (done) { + + var plugin = { + name: '--steve', + register: function (plugin, options, next) { + + plugin.path(); + next(); + } + }; + + var server = new Hapi.Server(); + expect(function () { + + server.pack.register(plugin, function (err) { }); + }).to.throw('path must be a non-empty string'); + done(); + }); + + it('registers required plugin', function (done) { var pack = new Hapi.Pack(); pack.server({ labels: ['s1', 'a', 'b'] }); @@ -201,7 +291,7 @@ describe('Pack', function () { pack.server({ labels: ['s3', 'a', 'b', 'd', 'cache'] }); pack.server({ labels: ['s4', 'b', 'test', 'cache'] }); - pack.require(__dirname + '/pack/--test1', {}, function (err) { + pack.register(require('./pack/--test1'), function (err) { expect(err).to.not.exist; @@ -217,12 +307,12 @@ describe('Pack', function () { }); }); - it('requires a plugin with options', function (done) { + it('registers a plugin with options', function (done) { var pack = new Hapi.Pack(); pack.server({ labels: ['a', 'b'] }); - pack.require('./pack/--test1', { something: true }, function (err) { + pack.register({ plugin: require('./pack/--test1'), options: { something: true } }, function (err) { expect(err).to.not.exist; done(); @@ -233,7 +323,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '2.0.0', register: function (plugin, options, next) { plugin.route({ method: 'GET', path: '/a', handler: function (request, reply) { reply('a'); } }); @@ -256,7 +345,7 @@ describe('Pack', function () { }); }); - it('requires multiple plugins using array', function (done) { + it('registers multiple plugins', function (done) { var server = new Hapi.Server({ labels: 'test' }); var log = null; @@ -265,56 +354,50 @@ describe('Pack', function () { log = [event, tags]; }); - server.pack.require(['./pack/--test1', './pack/--test2'], function (err) { + server.pack.register([require('./pack/--test1'), require('./pack/--test2')], function (err) { expect(err).to.not.exist; - expect(routesList(server)).to.deep.equal(['/test1', '/test2', '/test2/path']); + expect(routesList(server)).to.deep.equal(['/test1', '/test2']); expect(log[1].test).to.equal(true); expect(log[0].data).to.equal('abc'); done(); }); }); - it('requires multiple plugins using object', function (done) { + it('registers multiple plugins (verbose)', function (done) { var server = new Hapi.Server({ labels: 'test' }); - server.pack.require({ './pack/--test1': {}, './pack/--test2': {} }, function (err) { + var log = null; + server.pack.events.once('log', function (event, tags) { - expect(err).to.not.exist; - expect(routesList(server)).to.deep.equal(['/test1', '/test2', '/test2/path']); - done(); + log = [event, tags]; }); - }); - it('exposes the plugin path', function (done) { - - var server = new Hapi.Server({ labels: 'test' }); - server.pack.require('./pack/--test2', function (err) { + server.pack.register([{ plugin: require('./pack/--test1') }, { plugin: require('./pack/--test2') }], function (err) { expect(err).to.not.exist; - server.inject('/test2/path', function (res) { - - expect(res.result).to.equal(Path.join(process.cwd(), 'test', 'pack', '--test2')); - done(); - }); + expect(routesList(server)).to.deep.equal(['/test1', '/test2']); + expect(log[1].test).to.equal(true); + expect(log[0].data).to.equal('abc'); + done(); }); }); it('requires plugin with views', function (done) { var server = new Hapi.Server(); - server.pack.require({ './pack/--views': { message: 'viewing it' } }, function (err) { + server.pack.register({ plugin: require('./pack/--views'), options: { message: 'viewing it' } }, function (err) { expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/view' }, function (res) { + server.inject('/view', function (res) { expect(res.result).to.equal('

viewing it

'); - server.inject({ method: 'GET', url: '/file' }, function (res) { + server.inject('/file', function (res) { expect(res.result).to.equal('

{{message}}

'); - server.inject({ method: 'GET', url: '/ext' }, function (res) { + server.inject('/ext', function (res) { expect(res.result).to.equal('

grabbed

'); done(); @@ -328,7 +411,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '3.0.0', register: function (plugin, options, next) { plugin.views({ @@ -354,7 +436,7 @@ describe('Pack', function () { server.pack.register(plugin, function (err) { expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/view' }, function (res) { + server.inject('/view', function (res) { expect(res.result).to.equal('

steve

'); done(); @@ -362,31 +444,15 @@ describe('Pack', function () { }); }); - it('requires module', function (done) { - - var server = new Hapi.Server(); - server.pack.require('hapi-plugin-test', function (err) { - - expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/hapi/plugin/test' }, function (res) { - - expect(res.result).to.equal('hapi-plugin-test'); - expect(server.plugins['hapi-plugin-test'].path).to.equal(Path.join(process.cwd(), 'node_modules', 'hapi-plugin-test')); - done(); - }); - }); - }); - - it('requires a child plugin', function (done) { + it('registers a child plugin', function (done) { - var server = new Hapi.Server(); - server.pack.require('./pack/--child', function (err) { + var server = new Hapi.Server({ labels: 'test' }); + server.pack.register(require('./pack/--child'), function (err) { expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/hapi/plugin/test' }, function (res) { + server.inject('/test1', function (res) { - expect(res.result).to.equal('hapi-plugin-test'); - expect(server.plugins['hapi-plugin-test'].path).to.equal(Path.join(process.cwd(), 'node_modules', 'hapi-plugin-test')); + expect(res.result).to.equal('testing123'); done(); }); }); @@ -399,19 +465,7 @@ describe('Pack', function () { expect(function () { - pack.require('./pack/none', function (err) { }); - }).to.throw('Cannot find module'); - done(); - }); - - it('fails to require missing module in default route', function (done) { - - var pack = new Hapi.Pack(); - pack.server({ labels: ['a', 'b'] }); - - expect(function () { - - pack.require('none', function (err) { }); + pack.register(require('./pack/none'), function (err) { }); }).to.throw('Cannot find module'); done(); }); @@ -456,8 +510,8 @@ describe('Pack', function () { var pack = new Hapi.Pack(); expect(function () { - pack.register({ version: '0.0.0', register: function (plugin, options, next) { next(); } }, function (err) { }); - }).to.throw('Plugin missing name'); + pack.register({ register: function (plugin, options, next) { next(); } }, function (err) { }); + }).to.throw('Missing plugin name'); done(); }); @@ -466,7 +520,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '3.0.0', register: function (plugin, options, next) { plugin.route({ method: 'GET', path: '/b', handler: function (request, reply) { reply('b'); } }); @@ -486,7 +539,7 @@ describe('Pack', function () { expect(err).to.not.exist; expect(routesList(server)).to.deep.equal(['/b']); - server.inject({ method: 'GET', url: '/a' }, function (res) { + server.inject('/a', function (res) { expect(res.result).to.equal('b'); done(); @@ -510,7 +563,7 @@ describe('Pack', function () { pack._servers[1].route({ method: 'GET', path: '/', handler: handler }); pack._servers[2].route({ method: 'GET', path: '/', handler: handler }); - pack.require(['./pack/--deps1', './pack/--deps2', './pack/--deps3'], function (err) { + pack.register([require('./pack/--deps1'), require('./pack/--deps2'), require('./pack/--deps3')], function (err) { expect(err).to.not.exist; @@ -519,15 +572,15 @@ describe('Pack', function () { expect(err).to.not.exist; expect(pack.plugins['--deps1'].breaking).to.equal('bad'); - pack._servers[0].inject({ method: 'GET', url: '/' }, function (res) { + pack._servers[0].inject('/', function (res) { expect(res.result).to.equal('|2|1|') - pack._servers[1].inject({ method: 'GET', url: '/' }, function (res) { + pack._servers[1].inject('/', function (res) { expect(res.result).to.equal('|3|1|') - pack._servers[2].inject({ method: 'GET', url: '/' }, function (res) { + pack._servers[2].inject('/', function (res) { expect(res.result).to.equal('|3|2|') done(); @@ -538,67 +591,12 @@ describe('Pack', function () { }); }); - it('automatically resolves the requirePath if specified (relative)', function (done) { - - var pack = new Hapi.Pack({ requirePath: './pack' }); - pack.server({ labels: 'c' }); - - var handler = function (request, reply) { - - return reply(request.app.deps); - }; - - pack._servers[0].route({ method: 'GET', path: '/', handler: handler }); - - pack.require(['./pack/--deps3'], function (err) { - - expect(err).to.not.exist; - pack._servers[0].inject({ method: 'GET', url: '/' }, function (res) { - - expect(res.result).to.equal('|3|'); - done(); - }); - }); - }); - - it('automatically resolves the requirePath if specified (node_modules)', function (done) { - - var pack = new Hapi.Pack({ requirePath: process.cwd() + '/node_modules' }); - pack.server(); - pack.require('hapi-plugin-test', function (err) { - - expect(err).to.not.exist; - pack._servers[0].inject({ method: 'GET', url: '/hapi/plugin/test' }, function (res) { - - expect(res.result).to.equal('hapi-plugin-test'); - expect(pack._servers[0].plugins['hapi-plugin-test'].path).to.equal(Path.join(process.cwd(), 'node_modules', 'hapi-plugin-test')); - done(); - }); - }); - }); - - it('resolves relative path', function (done) { - - var pack = new Hapi.Pack(); - pack.server(); - pack.require('../node_modules/hapi-plugin-test', function (err) { - - expect(err).to.not.exist; - pack._servers[0].inject({ method: 'GET', url: '/hapi/plugin/test' }, function (res) { - - expect(res.result).to.equal('hapi-plugin-test'); - expect(pack._servers[0].plugins['hapi-plugin-test'].path).to.equal(Path.join(process.cwd(), 'node_modules', 'hapi-plugin-test')); - done(); - }); - }); - }); - it('fails to require single plugin with dependencies', function (done) { var server = new Hapi.Server(); expect(function () { - server.pack.require('./pack/--deps1', function (err) { }); + server.pack.register(require('./pack/--deps1'), function (err) { }); }).to.throw('Plugin --deps1 missing dependencies: --deps2'); done(); }); @@ -607,7 +605,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '3.0.0', register: function (plugin, options, next) { plugin.dependency('none'); @@ -636,14 +633,14 @@ describe('Pack', function () { domain.run(function () { - server.pack.require(['./pack/--deps1', './pack/--deps3'], function (err) { }); + server.pack.register([require('./pack/--deps1'), require('./pack/--deps3')], function (err) { }); }); }); it('fails to start server when after method fails', function (done) { var server = new Hapi.Server(); - server.pack.require('./pack/--afterErr', function (err) { + server.pack.register(require('./pack/--afterErr'), function (err) { expect(err).to.not.exist; server.start(function (err) { @@ -658,7 +655,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '1.0.0', register: function (plugin, options, next) { var cache = plugin.cache({ expiresIn: 10 }); @@ -711,11 +707,11 @@ describe('Pack', function () { var server = new Hapi.Server(); server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply('authenticated!') } }); - server.pack.require('./pack/--auth', function (err) { + server.pack.register(require('./pack/--auth'), function (err) { expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/' }, function (res) { + server.inject('/', function (res) { expect(res.statusCode).to.equal(401); server.inject({ method: 'GET', url: '/', headers: { authorization: 'Basic ' + (new Buffer('john:12345', 'utf8')).toString('base64') } }, function (res) { @@ -734,20 +730,9 @@ describe('Pack', function () { pack.server(0, { labels: 'a' }); pack.server(0, { labels: 'b' }); - pack.require('./pack/--auth', function (err) { - - expect(err).to.not.exist; - done(); - }); - }); - - it('requires a plugin using loader', function (done) { - - var server = new Hapi.Server(); - server.pack.require('./pack/--loader', function (err) { + pack.register(require('./pack/--auth'), function (err) { expect(err).to.not.exist; - expect(server.plugins['--inner']['way-down']).to.equal(42); done(); }); }); @@ -755,10 +740,10 @@ describe('Pack', function () { it('sets plugin context', function (done) { var server = new Hapi.Server(); - server.pack.require('./pack/--context', function (err) { + server.pack.register(require('./pack/--context'), function (err) { expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/' }, function (res) { + server.inject('/', function (res) { expect(res.result).to.equal('in context throughout'); done(); @@ -770,7 +755,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '2.0.0', register: function (plugin, options, next) { plugin.events.once('request', function (request, event, tags) { @@ -804,20 +788,18 @@ describe('Pack', function () { }); }); - it('sets route handler', function (done) { + it('sets directory route handler', function (done) { var server = new Hapi.Server({ files: { relativeTo: __dirname } }); - server.pack.require('./pack/--handler', function (err) { + server.pack.register(require('./pack/--handler'), function (err) { expect(err).to.not.exist; - var table = server.table(); - table.filter(function (route) { - if (route.path === '/handler/{file*}') { - expect(route.settings.handler).to.be.an('function'); - } - }); + server.inject('/handler/package.json', function (res) { - done(); + expect(res.statusCode).to.equal(200); + expect(JSON.parse(res.result).name).to.equal('--handler'); + done(); + }); }); }); @@ -936,7 +918,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '1.0.0', register: function (plugin, options, next) { plugin.method('log', function () { }); @@ -957,7 +938,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '1.0.0', register: function (plugin, options, next) { plugin.bind({ x: 1 }); @@ -980,7 +960,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '1.0.0', register: function (plugin, options, next) { plugin.method('log', function () { return this.x; }, { bind: { x: 2 } }); @@ -1002,7 +981,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '1.0.0', register: function (plugin, options, next) { plugin.bind({ x: 1 }); @@ -1023,7 +1001,6 @@ describe('Pack', function () { var a = { name: 'a', - version: '2.0.0', register: function (plugin, options, next) { plugin.dependency(['b', 'c']); @@ -1033,7 +1010,6 @@ describe('Pack', function () { var b = { name: 'b', - version: '2.0.0', register: function (plugin, options, next) { next(); @@ -1042,7 +1018,6 @@ describe('Pack', function () { var c = { name: 'c', - version: '2.0.0', register: function (plugin, options, next) { next(); @@ -1066,7 +1041,6 @@ describe('Pack', function () { var a = { name: 'a', - version: '2.0.0', register: function (plugin, options, next) { plugin.dependency('b'); @@ -1077,7 +1051,6 @@ describe('Pack', function () { var b = { name: 'b', - version: '2.0.0', register: function (plugin, options, next) { next(); @@ -1086,7 +1059,6 @@ describe('Pack', function () { var c = { name: 'c', - version: '2.0.0', register: function (plugin, options, next) { next(); @@ -1106,59 +1078,11 @@ describe('Pack', function () { }); }); - it('sets a loader then undo it', function (done) { - - var server = new Hapi.Server(); - - var plugin = { - name: 'test', - version: '1.0.0', - register: function (plugin, options, next) { - - plugin.loader(function () { throw new Error('bad'); }); - plugin.loader(); - plugin.require('./pack/--test2', next); - } - }; - - server.pack.register(plugin, function (err) { - - expect(err).to.not.exist; - server.inject('/test2/path', function (res) { - - expect(res.result).to.equal(Path.join(process.cwd(), 'test', 'pack', '--test2')); - done(); - }); - }); - }); - - it('throws when name is missing', function (done) { - - var server = new Hapi.Server(); - - expect(function () { - - server.pack.require(null, function (err) { }); - }).to.throw('Invalid plugin name(s) object: must be string, object, or array'); - done(); - }); - - it('throws when name is invalid type', function (done) { - - var server = new Hapi.Server(); - - expect(function () { - - server.pack.require(45, function (err) { }); - }).to.throw('Invalid plugin name(s) object: must be string, object, or array'); - done(); - }); - it('starts a pack without callback', function (done) { var pack = new Hapi.Pack(); pack.server(0); - pack.require('./pack/--afterErr', function (err) { + pack.register(require('./pack/--afterErr'), function (err) { pack.start(); setTimeout(function () { @@ -1251,7 +1175,6 @@ describe('Pack', function () { var server = new Hapi.Server(); var plugin = { name: 'foo', - version: '0.0.1', register: function (plugin, options, next) { plugin.handler('bar', function (route, options) { @@ -1312,7 +1235,7 @@ describe('Pack', function () { } ], plugins: { - '../test/pack/--test1': [{ ext: true }, {}] + '../test/pack/--test1': null } }; @@ -1324,7 +1247,7 @@ describe('Pack', function () { expect(err).to.not.exist; pack.stop(function () { - pack._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { + pack._servers[0].inject('/test1', function (res) { expect(res.result).to.equal('testing123special-value'); done(); @@ -1373,7 +1296,7 @@ describe('Pack', function () { expect(err).to.not.exist; pack.stop(); - pack._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { + pack._servers[0].inject('/test1', function (res) { expect(res.result).to.equal('testing123'); done(); @@ -1382,6 +1305,57 @@ describe('Pack', function () { }); }); + it('composes pack (relative and absolute paths)', function (done) { + + var manifest = { + pack: { + cache: { + engine: 'catbox-memory' + }, + app: { + my: 'special-value' + } + }, + servers: [ + { + port: 0, + options: { + labels: ['api', 'nasty', 'test'] + } + }, + { + host: 'localhost', + port: 0, + options: { + labels: ['api', 'nice'] + } + } + ], + plugins: { + './--test2': null + } + }; + + manifest.plugins[__dirname + '/pack/--test1'] = null; + + Hapi.Pack.compose(manifest, { relativeTo: __dirname + '/pack' }, function (err, pack) { + + expect(err).to.not.exist; + pack.start(function (err) { + + expect(err).to.not.exist; + pack.stop(function () { + + pack._servers[0].inject('/test1', function (res) { + + expect(res.result).to.equal('testing123special-value'); + done(); + }); + }); + }); + }); + }); + it('composes pack with ports', function (done) { var manifest = { @@ -1412,18 +1386,8 @@ describe('Pack', function () { expect(function () { Hapi.Pack.compose(manifest, function (err, pack) { }); - }).to.throw('Pack missing servers definition'); + }).to.throw('Manifest missing servers definition'); done(); }); - - it('composes pack with default pack settings', function (done) { - - Hapi.Pack.compose({ servers: [{}], plugins: {} }, { app: 'only here' }, function (err, pack) { - - expect(err).to.not.exist; - expect(pack.app).to.equal('only here'); - done(); - }); - }); }); }); diff --git a/test/pack/--afterErr/index.js b/test/pack/--afterErr/index.js index 1b8c26b2b..53c2d766c 100755 --- a/test/pack/--afterErr/index.js +++ b/test/pack/--afterErr/index.js @@ -14,3 +14,8 @@ exports.register = function (plugin, options, next) { return next(); }; + + +exports.register.attributes = { + pkg: require('./package.json') +}; diff --git a/test/pack/--auth/index.js b/test/pack/--auth/index.js index 6e76a46a1..d313f0a89 100755 --- a/test/pack/--auth/index.js +++ b/test/pack/--auth/index.js @@ -32,6 +32,11 @@ exports.register = function (plugin, options, next) { }; +exports.register.attributes = { + pkg: require('./package.json') +}; + + internals.implementation = function (server, options) { var settings = Hoek.clone(options); diff --git a/test/pack/--child/lib/index.js b/test/pack/--child/lib/index.js index 3c174743a..139772845 100755 --- a/test/pack/--child/lib/index.js +++ b/test/pack/--child/lib/index.js @@ -7,6 +7,11 @@ var internals = {}; exports.register = function (plugin, options, next) { - plugin.require('hapi-plugin-test', {}, next); + plugin.register(require('../../--test1'), {}, next); +}; + + +exports.register.attributes = { + pkg: require('../package.json') }; diff --git a/test/pack/--context/lib/index.js b/test/pack/--context/lib/index.js index f7ea0e03f..70b0efabc 100755 --- a/test/pack/--context/lib/index.js +++ b/test/pack/--context/lib/index.js @@ -31,3 +31,8 @@ exports.register = function (plugin, options, next) { next(); }; + +exports.register.attributes = { + pkg: require('../package.json') +}; + diff --git a/test/pack/--deps1/index.js b/test/pack/--deps1/index.js index 9af5ad72b..2e364acd7 100755 --- a/test/pack/--deps1/index.js +++ b/test/pack/--deps1/index.js @@ -20,6 +20,11 @@ exports.register = function (plugin, options, next) { }; +exports.register.attributes = { + pkg: require('./package.json') +}; + + internals.after = function (plugin, next) { plugin.expose('breaking', plugin.plugins['--deps2'].breaking); diff --git a/test/pack/--deps2/index.js b/test/pack/--deps2/index.js index aa611b3bf..616b71229 100755 --- a/test/pack/--deps2/index.js +++ b/test/pack/--deps2/index.js @@ -18,3 +18,8 @@ exports.register = function (plugin, options, next) { return next(); }; + + +exports.register.attributes = { + pkg: require('./package.json') +}; diff --git a/test/pack/--deps3/index.js b/test/pack/--deps3/index.js index 99d2f92b5..209fbb2df 100755 --- a/test/pack/--deps3/index.js +++ b/test/pack/--deps3/index.js @@ -16,3 +16,8 @@ exports.register = function (plugin, options, next) { return next(); }; + + +exports.register.attributes = { + pkg: require('./package.json') +}; diff --git a/test/pack/--handler/index.js b/test/pack/--handler/index.js index 569dabe81..f837acfbc 100755 --- a/test/pack/--handler/index.js +++ b/test/pack/--handler/index.js @@ -7,15 +7,22 @@ var internals = {}; exports.register = function (plugin, options, next) { + plugin.path(__dirname); + plugin.route({ method: 'GET', path: '/handler/{file*}', handler: { directory: { - path: 'pack/--handler/' + path: './' } } }); return next(); }; + + +exports.register.attributes = { + pkg: require('./package.json') +}; diff --git a/test/pack/--loaded/lib/index.js b/test/pack/--loaded/lib/index.js old mode 100644 new mode 100755 index adcc5dde9..c6b7aac0d --- a/test/pack/--loaded/lib/index.js +++ b/test/pack/--loaded/lib/index.js @@ -11,3 +11,8 @@ exports.register = function (plugin, options, next) { return next(); }; + + +exports.register.attributes = { + pkg: require('../package.json') +}; diff --git a/test/pack/--loader/lib/index.js b/test/pack/--loader/lib/index.js deleted file mode 100755 index 504678415..000000000 --- a/test/pack/--loader/lib/index.js +++ /dev/null @@ -1,13 +0,0 @@ -// Declare internals - -var internals = {}; - - -// Plugin registration - -exports.register = function (plugin, options, next) { - - plugin.loader(require); - plugin.require('--inner', next); -}; - diff --git a/test/pack/--loader/node_modules/--inner/lib/index.js b/test/pack/--loader/node_modules/--inner/lib/index.js deleted file mode 100755 index 123a1f47d..000000000 --- a/test/pack/--loader/node_modules/--inner/lib/index.js +++ /dev/null @@ -1,13 +0,0 @@ -// Declare internals - -var internals = {}; - - -// Plugin registration - -exports.register = function (plugin, options, next) { - - plugin.expose('way-down', 42); - next(); -}; - diff --git a/test/pack/--loader/node_modules/--inner/package.json b/test/pack/--loader/node_modules/--inner/package.json deleted file mode 100755 index 969585087..000000000 --- a/test/pack/--loader/node_modules/--inner/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "--inner", - "description": "Test plugin module", - "version": "0.0.1", - "private": true, - "main": "lib/index" -} diff --git a/test/pack/--loader/package.json b/test/pack/--loader/package.json deleted file mode 100755 index da0b9ce8b..000000000 --- a/test/pack/--loader/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "--local", - "description": "Test plugin module", - "version": "0.0.1", - "private": true, - "main": "lib/index" -} diff --git a/test/pack/--skip/lib/index.js b/test/pack/--skip/lib/index.js index bb524e8d6..d6d9c59b4 100755 --- a/test/pack/--skip/lib/index.js +++ b/test/pack/--skip/lib/index.js @@ -7,3 +7,8 @@ exports.register = function (plugin, next) { return next(); }; + + +exports.register.attributes = { + pkg: require('../package.json') +}; diff --git a/test/pack/--test1/lib/index.js b/test/pack/--test1/lib/index.js index a9edc5772..d38d00e90 100755 --- a/test/pack/--test1/lib/index.js +++ b/test/pack/--test1/lib/index.js @@ -15,6 +15,12 @@ exports.register = function (plugin, options, next) { }; +exports.register.attributes = { + name: '--test1', + version: '1.0.0' +}; + + internals.math = { add: function (a, b) { diff --git a/test/pack/--test2/lib/index.js b/test/pack/--test2/lib/index.js index c1dd153e5..648a31a10 100755 --- a/test/pack/--test2/lib/index.js +++ b/test/pack/--test2/lib/index.js @@ -8,8 +8,12 @@ var internals = {}; exports.register = function (plugin, options, next) { plugin.route({ path: '/test2', method: 'GET', handler: function (request, reply) { reply('testing123'); } }); - plugin.route({ path: '/test2/path', method: 'GET', handler: function (request, reply) { reply(plugin.path); } }); plugin.log('test', 'abc'); return next(); }; + +exports.register.attributes = { + pkg: require('../package.json') +}; + diff --git a/test/pack/--views/lib/index.js b/test/pack/--views/lib/index.js index c3e37ad6c..115fb765d 100755 --- a/test/pack/--views/lib/index.js +++ b/test/pack/--views/lib/index.js @@ -1,3 +1,8 @@ +// Load modules + +var Path = require('path'); + + // Declare internals var internals = {}; @@ -7,6 +12,8 @@ var internals = {}; exports.register = function (plugin, options, next) { + plugin.path(Path.join(__dirname, '..')); + plugin.views({ engines: { 'html': require('handlebars') }, path: './templates' @@ -35,3 +42,8 @@ exports.register = function (plugin, options, next) { return next(); }; + + +exports.register.attributes = { + pkg: require('../package.json') +}; From 78a2b3721f9d361e8c397628e091b4b50b732b0c Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 25 May 2014 19:40:03 -0700 Subject: [PATCH 06/33] Partial docs --- docs/Reference.md | 154 +++++++++++++++++++--------------------------- 1 file changed, 65 insertions(+), 89 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index 7624e1b4c..f0f906313 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -65,9 +65,7 @@ - [`pack.server([host], [port], [options])`](#packserverhost-port-options) - [`pack.start([callback])`](#packstartcallback) - [`pack.stop([options], [callback])`](#packstopoptions-callback) - - [`pack.require(name, options, callback)`](#packrequirename-options-callback) - - [`pack.require(names, callback)`](#packrequirenames-callback) - - [`pack.register(plugin, options, callback)`](#packregisterplugin-options-callback) + - [`pack.register(plugins, options, callback)`](#packregisterplugins-options-callback) - [`Pack.compose(manifest, [options], callback)`](#Packcomposemanifest-options-callback) - [Plugin interface](#plugin-interface) - [`exports.register(plugin, options, next)`](#exportsregisterplugin-options-next) @@ -88,7 +86,6 @@ - [`plugin.cache(options)`](#plugincacheoptions) - [`plugin.require(name, options, callback)`](#pluginrequirename-options-callback) - [`plugin.require(names, callback)`](#pluginrequirenames-callback) - - [`plugin.loader(require)`](#pluginloader-require) - [`plugin.bind(bind)`](#pluginbind-bind) - [`plugin.handler(name, method)`](#pluginhandlername-method) - [Selectable methods and properties](#selectable-methods-and-properties) @@ -2072,7 +2069,7 @@ Creates a new `Pack` object instance where: - `options` - optional configuration: - `app` - an object used to initialize the application-specific data stored in `pack.app`. - `cache` - cache configuration as described in the server [`cache`](#server.config.cache) option. - - `requirePath` - sets the path from which node module plugins are loaded. Applies only when using [`pack.require()`](#packrequirename-options-callback) + - `requirePath` - sets the path from which node module plugins are loaded. Applies only when using [`pack.register()`](#packregisterplugins-options-callback) with module names that do no include a relative or absolute path (e.g. 'lout'). Defaults to the node module behaviour described in [node modules](http://nodejs.org/api/modules.html#modules_loading_from_node_modules_folders). Note that if the modules are located inside a 'node_modules' sub-directory, `requirePath` must end with `'/node_modules'`. @@ -2095,6 +2092,8 @@ Each `Pack` object instance has the following properties: - `version` - plugin version. - `path` - the plugin root path (where 'package.json' is located). - `register()` - the [`exports.register()`](#exportsregisterplugin-options-next) function. +- `plugins` - an object where each key is a plugin name and the value are the exposed properties by that plugin using + [`plugin.expose()`](#pluginexposekey-value). ### `Pack` methods @@ -2167,7 +2166,7 @@ pack.require('furball', { version: '/v' }, function (err) { Registers a list of plugins where: -- `names` - an array of plugins names as described in [`pack.require()`](#packrequirename-options-callback), or an object in which +- `names` - an array of plugins names as described in [`pack.register()`](#packregisterplugins-options-callback), or an object in which each key is a plugin name, and each value is the `options` object used to register that plugin. - `callback` - the callback function with signature `function(err)` where: - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts @@ -2192,38 +2191,61 @@ pack.require({ furball: null, lout: { endpoint: '/docs' } }, function (err) { }); ``` -#### `pack.register(plugin, options, callback)` +#### `pack.register(plugins, [options], callback)` -Registers a plugin object (without using `require()`) where: +Registers a plugin where: -- `plugin` - the plugin object which requires: - - `name` - plugin name. - - `version` - plugin version. - - `path` - optional plugin path for resolving relative paths used by the plugin. Defaults to current working directory. - - `register()` - the [`exports.register()`](#exportsregisterplugin-options-next) function. -- `options` - optional configuration object which is passed to the plugin via the `options` argument in - [`exports.register()`](#exportsregisterplugin-options-next). +- `plugins` - an object or array of objects with the following: + - `name` - plugin name. Required unless the `register()` function has `attributes` as described in + [Plugin interface](#Plugin-interface). + - `version` - an optional plugin version. Defaults to `'0.0.0'`. + - `register` - the [`register()`](#exportsregisterplugin-options-next) function. Cannot appear together with `plugin` but + one is required. + - `plugin` - an object (usually obtained by calling node's `require()`) with: + - `register` - the [`exports.register()`](#exportsregisterplugin-options-next) function. + - `options` - optional configuration object which is passed to the plugin via the `options` argument in + [`exports.register()`](#exportsregisterplugin-options-next). +- `options` - optional registration options (used by **hapi** and is not passed to the plugin). - `callback` - the callback function with signature `function(err)` where: - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts (e.g. among routes, methods, state) will throw an error and will not return a callback. ```javascript -var plug = { +// Manually constructed plugin object + +var plugin = { name: 'test', version: '2.0.0', register: function (plugin, options, next) { plugin.route({ method: 'GET', path: '/special', handler: function (request, reply) { reply(options.message); } } ); next(); + }, + options: { + message: 'hello' } }; -server.pack.register(plug, { message: 'hello' }, function (err) { +server.pack.register(plug, function (err) { if (err) { console.log('Failed loading plugin'); } }); + +// Module plugin object + +server.pack.register({ + plugin: require('plugin_name'), + options: { + message: 'hello' + } + }, function (err) { + + if (err) { + console.log('Failed loading plugin'); + } + }); ``` ### `Pack.compose(manifest, [options], callback)` @@ -2238,7 +2260,11 @@ and registering plugins where: `cache` option is not allowed and must be configured via the pack `cache` option. The `host` and `port` keys can be set to an environment variable by prefixing the variable name with `'$env.'`. - `plugins` - an object where each key is a plugin name, and each value is the `options` object used to register that plugin. -- `options` - optional pack configuration used as the baseline configuration for the pack (`manifest.pack` is applied to `options`). +- `options` - optional compose configuration: + - `relativeTo` - path prefix used when loading plugins using node's `require()`. The `relativeTo` path prefix is added to any + relative plugin name (i.e. beings with `'./'`). All other module names are required as-is and will be looked up from the location + of the **hapi** module path (e.g. if **hapi** resides outside of your project `node_modules` path, it will not find your project + dependencies - you should specify them as relative and use the `relativeTo` option). - `callback` - the callback method, called when all packs and servers have been created and plugins registered has the signature `function(err, pack)` where: - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts @@ -2284,28 +2310,21 @@ Hapi.Pack.composer(manifest, function (err, pack) { ## Plugin interface -Plugins provide an extensibility platform for both general purpose utilities such as [batch requests](https://github.com/spumko/bassmaster) and for -application business logic. Instead of thinking about a web server as a single entity with a unified routing table, plugins enable developers to -break their application into logical units, assembled together in different combinations to fit the development, testing, and deployment needs. +Plugins provide an extensibility platform for both general purpose utilities such as [batch requests](https://github.com/spumko/bassmaster) +and for application business logic. Instead of thinking about a web server as a single entity with a unified routing table, plugins enable +developers to break their application into logical units, assembled together in different combinations to fit the development, testing, and +deployment needs. Constructing a plugin requires the following: -- name - the plugin name is used as a unique key. Public plugins should be published in the [npm registry](https://npmjs.org) and derive their name - from the registry name. This ensures uniqueness. Private plugin names should be picked carefully to avoid conflicts with both private and public - names. Typically, private plugin names use a prefix such as the company name or an unusual combination of characters (e.g. `'--'`). When using the - [`pack.require()`](#packrequirename-options-callback) interface, the name is obtained from the 'package.json' module file. When using the - [`pack.register()`](#packregisterplugin-options-callback) interface, the name is provided as a required key in `plugin`. -- version - the plugin version is only used informatively within the framework but plays an important role in the plugin echo-system. The plugin - echo-system relies on the [npm peer dependency](http://blog.nodejs.org/2013/02/07/peer-dependencies/) functionality to ensure that plugins can - specify their dependency on a specific version of **hapi**, as well as on each other. Dependencies are expressed solely within the 'package.json' - file, and are enforced by **npm**. When using the [`pack.require()`](#packrequirename-options-callback) interface, the version is obtained from - the 'package.json' module file. When using the [`pack.register()`](#packregisterplugin-options-callback) interface, the version is provided as - a required key in `plugin`. -- `exports.register()` - the registration function described in [`exports.register()`](#exportsregisterplugin-options-next) is the plugin's core. - The function is called when the plugin is registered and it performs all the activities required by the plugin to operate. It is the single entry - point into the plugin functionality. When using the [`pack.require()`](#packrequirename-options-callback) interface, the function is obtained by - [`require()`](http://nodejs.org/api/modules.html#modules_module_require_id)'ing the plugin module and invoking the exported `register()` method. - When using the [`pack.register()`](#packregisterplugin-options-callback) interface, the function is provided as a required key in `plugin`. +- name - the plugin name is used as a unique key. Public plugins should be published in the [npm registry](https://npmjs.org) and derive + their name from the registry name to ensure uniqueness. Private plugin names should be picked carefully to avoid conflicts with both + private and public names. +- registeration function - the function described in [`exports.register()`](#exportsregisterplugin-options-next) is the plugin's core. + The function is called when the plugin is registered and it performs all the activities required by the plugin to operate. It is the + single entry point into the plugin's functionality. +- version - the optional plugin version is only used informatively to enable other plugins to find out the verions loaded. The version + should be the same as the one specified in the plugin's 'package.json' file. **package.json** @@ -2669,51 +2688,6 @@ exports.register = function (plugin, options, next) { }; ``` -#### `plugin.require(name, [options], callback)` - -Registers a plugin with the same pack as the current plugin following the syntax of [`pack.require()`](#packrequirename-options-callback). - -```javascript -exports.register = function (plugin, options, next) { - - plugin.require('furball', { version: '/v' }, function (err) { - - next(err); - }); -}; -``` - -#### `plugin.require(names, callback)` - -Registers a list of plugins with the same pack following the syntax of [`pack.require()`](#packrequirename-callback). - -```javascript -exports.register = function (plugin, options, next) { - - plugin.require(['furball', 'lout'], function (err) { - - next(err); - }); -}; -``` - -#### `plugin.loader(require)` - -Forces using the local `require()` method provided by node when calling `plugin.require()`. This sets the module path relative -to the plugin instead of relative to the hapi framework module location. This is needed to work around the limitations in node's -`require()`. - -```javascript -exports.register = function (plugin, options, next) { - - plugin.loader(require); - plugin.require('furball', { version: '/v' }, function (err) { - - next(err); - }); -}; -``` - #### `plugin.bind(bind)` Sets a global plugin bind used as the default bind when adding a route or an extension using the plugin interface (if no @@ -2968,16 +2942,18 @@ console.log(Hapi.version); ## `Hapi CLI` -The **hapi** command line interface allows a pack of servers to be composed and started from a configuration file only from the command line. -When installing **hapi** with the global flag the **hapi** binary script will be installed in the path. The following arguments are available to the -**hapi** CLI: +The **hapi** command line interface allows a pack of servers to be composed and started from a configuration file +only from the command line. When installing **hapi** with the global flag the **hapi** binary script will be +installed in the path. The following arguments are available to the **hapi** CLI: - '-c' - the path to configuration json file (required) - '-p' - the path to the node_modules folder to load plugins from (optional) - '--require' - a module the cli will require before hapi is required (optional) ex. loading a metrics library -Note that `--require` will require from node_modules, an absolute path, a relative path, or from the node_modules set by `-p` if available. - -In order to help with A/B testing there is [confidence](https://github.com/spumko/confidence). Confidence is a configuration document format, an API, and a foundation for A/B testing. The configuration format is designed to work with any existing JSON-based configuration, serving values based on object path ('/a/b/c' translates to a.b.c). In addition, confidence defines special $-prefixed keys used to filter values for a given criteria. - +Note that `--require` will require from node_modules, an absolute path, a relative path, or from the node_modules +set by `-p` if available. +In order to help with A/B testing there is [confidence](https://github.com/spumko/confidence). Confidence is a +configuration document format, an API, and a foundation for A/B testing. The configuration format is designed to +work with any existing JSON-based configuration, serving values based on object path ('/a/b/c' translates to a.b.c). +In addition, confidence defines special $-prefixed keys used to filter values for a given criteria. From affe9bef35af87759ba2f48389a7a40d08698838 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 25 May 2014 23:13:08 -0700 Subject: [PATCH 07/33] Update register plugin format --- docs/Reference.md | 250 ++++++++++++++++++++-------------------------- lib/pack.js | 5 +- test/pack.js | 3 +- 3 files changed, 110 insertions(+), 148 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index f0f906313..3ac2510f0 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -70,12 +70,12 @@ - [Plugin interface](#plugin-interface) - [`exports.register(plugin, options, next)`](#exportsregisterplugin-options-next) - [Root methods and properties](#root-methods-and-properties) - - [`plugin.version`](#pluginversion) - - [`plugin.path`](#pluginpath) - [`plugin.hapi`](#pluginhapi) + - [`plugin.version`](#pluginversion) - [`plugin.app`](#pluginapp) - [`plugin.events`](#pluginevents) - [`plugin.plugins`](#pluginplugins) + - [`plugin.path(path)`](#pluginpathpath) - [`plugin.log(tags, [data, [timestamp]])`](#pluginlogtags-data-timestamp) - [`plugin.dependency(deps, [after])`](#plugindependencydeps-after) - [`plugin.after(method)`](#pluginaftermethod) @@ -84,8 +84,7 @@ - [`plugin.method(method)`](#pluginmethodmethod) - [`plugin.methods`](#pluginmethods) - [`plugin.cache(options)`](#plugincacheoptions) - - [`plugin.require(name, options, callback)`](#pluginrequirename-options-callback) - - [`plugin.require(names, callback)`](#pluginrequirenames-callback) + - [`plugin.register(plugins, options, callback)`](#pluginregisterplugins-options-callback) - [`plugin.bind(bind)`](#pluginbind-bind) - [`plugin.handler(name, method)`](#pluginhandlername-method) - [Selectable methods and properties](#selectable-methods-and-properties) @@ -111,9 +110,10 @@ Creates a new server instance with the following arguments: -- `host` - the hostname, IP address, or path to UNIX domain socket the server is bound to. Defaults to `0.0.0.0` which means any available network - interface. Set to `127.0.0.1` or `localhost` to restrict connection to those coming from the same machine. If `host` contains a '/' character, it - is used as a UNIX domain socket path and if it starts with '\\.\pipe' as a Windows named pipe. +- `host` - the hostname, IP address, or path to UNIX domain socket the server is bound to. Defaults to `0.0.0.0` which + means any available network interface. Set to `127.0.0.1` or `localhost` to restrict connection to those coming from + the same machine. If `host` contains a '/' character, it is used as a UNIX domain socket path and if it starts with + '\\.\pipe' as a Windows named pipe. - `port` - the TPC port the server is listening to. Defaults to port `80` for HTTP and to `443` when TLS is configured. To use an ephemeral port, use `0` and once the server is started, retrieve the port allocation via `server.info.port`. - `options` - An object with the server configuration as described in [server options](#server-options). @@ -136,29 +136,32 @@ var server = Hapi.createServer('localhost', 8000, { cors: true }); When creating a server instance, the following options configure the server's behavior: -- `app` - application-specific configuration which can later be accessed via `server.settings.app`. Provides a safe place to store application configuration without potential conflicts with **hapi**. Should not be used by plugins which should use `plugins[name]`. Note the difference between - `server.settings.app` which is used to store configuration value and `server.app` which is meant for storing run-time state. +- `app` - application-specific configuration which can later be accessed via `server.settings.app`. Provides a safe + place to store application configuration without potential conflicts with **hapi**. Should not be used by plugins which + should use `plugins[name]`. Note the difference between `server.settings.app` which is used to store configuration value + and `server.app` which is meant for storing run-time state. -- `cache` - determines the type of server-side cache used. Every server includes a default cache for storing and - application state. By default, a simple memory-based cache is created which has a limited capacity. **hapi** uses - [**catbox** module documentation](https://github.com/spumko/catbox#client) as its cache implementation which includes support for Redis, MongoDB, - Memcached, and Riak. Caching is only utilized if methods and plugins explicitly store their state in the cache. The server cache configuration - only defines the store itself. `cache` can be assigned: +- `cache` - determines the type of server-side cache used. Every server includes a default + cache for storing and application state. By default, a simple memory-based cache is created which has a limited capacity. + **hapi** uses [**catbox** module documentation](https://github.com/spumko/catbox#client) as its cache implementation which + includes support for Redis, MongoDB, Memcached, and Riak. Caching is only utilized if methods and plugins explicitly store + their state in the cache. The server cache configuration only defines the store itself. `cache` can be assigned: - a string with the cache engine module name (e.g. `'catbox-memory'`, `'catbox-redis'`). - a configuration object with the following options: - `engine` - - cache options. The cache options are described in the [**catbox** module documentation](https://github.com/spumko/catbox#client). When an array - of options is provided, multiple cache connections are established and each array item (except one) must include an additional option: - - `name` - an identifier used later when provisioning or configuring caching for routes, methods, or plugins. Each connection name must be unique. A - single item may omit the `name` option which defines the default cache. If every connection includes a `name`, a default memory cache is provisions - as well as the default. - - `shared` - if `true`, allows multiple cache users to share the same segment (e.g. multiple servers in a pack using the same route and cache. - Default to not shared. + cache options. The cache options are described in the [**catbox** module documentation](https://github.com/spumko/catbox#client). + When an array of options is provided, multiple cache connections are established and each array item (except one) must + include an additional option: + - `name` - an identifier used later when provisioning or configuring caching for routes, methods, or plugins. Each + connection name must be unique. A single item may omit the `name` option which defines the default cache. If every + connection includes a `name`, a default memory cache is provisions as well as the default. + - `shared` - if `true`, allows multiple cache users to share the same segment (e.g. multiple servers in a pack using + the same route and cache. Default to not shared. - an array of the above types for configuring multiple cache instances, each with a unqiue name. -- `cors` - the [Cross-Origin Resource Sharing](http://www.w3.org/TR/cors/) protocol allows browsers to make cross-origin API calls. CORS is - required by web applications running inside a browser which are loaded from a different domain than the API server. CORS headers are disabled by - default. To enable, set `cors` to `true`, or to an object with the following options: +- `cors` - the [Cross-Origin Resource Sharing](http://www.w3.org/TR/cors/) protocol allows browsers to make cross-origin API + calls. CORS is required by web applications running inside a browser which are loaded from a different domain than the API + server. CORS headers are disabled by default. To enable, set `cors` to `true`, or to an object with the following options: - `origin` - a strings array of allowed origin servers ('Access-Control-Allow-Origin'). The array can contain any combination of fully qualified origins along with origin strings containing a wilcard '*' character, or a single `'*'` origin string. Defaults to any origin `['*']`. - `isOriginExposed` - if `false`, prevents the server from returning the full list of non-wildcard `origin` values if the incoming origin header @@ -2195,57 +2198,60 @@ pack.require({ furball: null, lout: { endpoint: '/docs' } }, function (err) { Registers a plugin where: -- `plugins` - an object or array of objects with the following: - - `name` - plugin name. Required unless the `register()` function has `attributes` as described in - [Plugin interface](#Plugin-interface). - - `version` - an optional plugin version. Defaults to `'0.0.0'`. - - `register` - the [`register()`](#exportsregisterplugin-options-next) function. Cannot appear together with `plugin` but - one is required. - - `plugin` - an object (usually obtained by calling node's `require()`) with: - - `register` - the [`exports.register()`](#exportsregisterplugin-options-next) function. - - `options` - optional configuration object which is passed to the plugin via the `options` argument in - [`exports.register()`](#exportsregisterplugin-options-next). +- `plugins` - a plugin object or array of plugin objects. The objects can use one of two formats: + - a module plugin object. + - a manually constructed plugin object. - `options` - optional registration options (used by **hapi** and is not passed to the plugin). - `callback` - the callback function with signature `function(err)` where: - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts (e.g. among routes, methods, state) will throw an error and will not return a callback. +Module plugin is registered by passing the following object (or array of object) as `plugins`: +- `plugin` - an object (usually obtained by calling node's `require()`) with: + - `register` - the [`exports.register()`](#exportsregisterplugin-options-next) function. The function must have an `attributes` + property with either `name` (and optional `version`) keys or `pkg` with the content of the module's 'package.json'. +- `options` - optional configuration object which is passed to the plugin via the `options` argument in + [`exports.register()`](#exportsregisterplugin-options-next). + ```javascript -// Manually constructed plugin object +server.pack.register({ + plugin: require('plugin_name'), + options: { + message: 'hello' + } + }, function (err) { -var plugin = { + if (err) { + console.log('Failed loading plugin'); + } + }); +``` + +Manually constructed plugin is an object containing: +- `name` - plugin name. +- `version` - an optional plugin version. Defaults to `'0.0.0'`. +- `register` - the [`register()`](#exportsregisterplugin-options-next) function. +- `options` - optional configuration object which is passed to the plugin via the `options` argument in + [`exports.register()`](#exportsregisterplugin-options-next). + +```javascript +server.pack.register({ name: 'test', version: '2.0.0', register: function (plugin, options, next) { - plugin.route({ method: 'GET', path: '/special', handler: function (request, reply) { reply(options.message); } } ); + plugin.route({ method: 'GET', path: '/special', handler: function (request, reply) { reply(options.message); } }); next(); }, options: { message: 'hello' } -}; - -server.pack.register(plug, function (err) { +}, function (err) { if (err) { console.log('Failed loading plugin'); } }); - -// Module plugin object - -server.pack.register({ - plugin: require('plugin_name'), - options: { - message: 'hello' - } - }, function (err) { - - if (err) { - console.log('Failed loading plugin'); - } - }); ``` ### `Pack.compose(manifest, [options], callback)` @@ -2315,7 +2321,7 @@ and for application business logic. Instead of thinking about a web server as a developers to break their application into logical units, assembled together in different combinations to fit the development, testing, and deployment needs. -Constructing a plugin requires the following: +A plugin is constructed with the following: - name - the plugin name is used as a unique key. Public plugins should be published in the [npm registry](https://npmjs.org) and derive their name from the registry name to ensure uniqueness. Private plugin names should be picked carefully to avoid conflicts with both @@ -2326,80 +2332,35 @@ Constructing a plugin requires the following: - version - the optional plugin version is only used informatively to enable other plugins to find out the verions loaded. The version should be the same as the one specified in the plugin's 'package.json' file. -**package.json** - -```json -{ - "name": "furball", - "description": "Plugin utilities and endpoints", - "version": "0.3.0", - "main": "index", - "dependencies": { - "hoek": "0.8.x" - }, - "peerDependencies": { - "hapi": "1.x.x" - } -} -``` - -**index.js** +The name and versions are included by attaching an `attributes` property to the `exports.register()` function: ```javascript -var Hoek = require('hoek'); - -var internals = { - defaults: { - version: '/version', - plugins: '/plugins' - } -}; - -internals.version = 1.1; - exports.register = function (plugin, options, next) { - var settings = Hoek.applyToDefaults(internals.defaults, options); - - if (settings.version) { - plugin.route({ - method: 'GET', - path: settings.version, - handler: function (request, reply) { - - reply(internals.version); - } - }); - } - - if (settings.plugins) { - plugin.route({ - method: 'GET', - path: settings.plugins, - handler: function (request, reply) { - - reply(listPlugins(request.server)); - } - }); - } + plugin.route({ + method: 'GET', + path: '/version', + handler: function (request, reply) { - var listPlugins = function (server) { + reply('1.0.0'); + } + }); - var plugins = []; - Object.keys(server.pack.list).forEach(function (name) { + next(); +}; - var item = server.pack.list[name]; - plugins.push({ - name: item.name, - version: item.version - }); - }); +exports.register.attributes = { + name: 'example', + version: '1.0.0' +}; +``` - return plugins; - }; +Alternatively, the name and version can be included via the `pkg` attribute containing the 'package.json' file for the module which +already has the name and version included: - plugin.expose('plugins', listPlugins); - next(); +```javascript +exports.register.attributes = { + pkg: require('./package.json') }; ``` @@ -2428,47 +2389,33 @@ The plugin interface root methods and properties are those available only on the [`exports.register()`](#exportsregisterplugin-options-next) interface. They are not available on the object received by calling [`plugin.select()`](#pluginselectlabels). -#### `plugin.version` +#### `plugin.hapi` -The plugin version information. +A reference to the **hapi** module used to create the pack and server instances. Removes the need to add a dependency on **hapi** within the plugin. ```javascript exports.register = function (plugin, options, next) { - console.log(plugin.version); - next(); -}; -``` - -#### `plugin.path` - -The plugin root path (where 'package.json' resides). + var Hapi = plugin.hapi; -```javascript -var Fs = require('fs'); + var handler = function (request, reply) { -exports.register = function (plugin, options, next) { + reply(Hapi.error.internal('Not implemented yet')); + }; - var file = Fs.readFileSync(plugin.path + '/resources/image.png'); + plugin.route({ method: 'GET', path: '/', handler: handler }); next(); }; ``` -#### `plugin.hapi` +#### `plugin.version` -A reference to the **hapi** module used to create the pack and server instances. Removes the need to add a dependency on **hapi** within the plugin. +The **hapi** version used to load the plugin. ```javascript exports.register = function (plugin, options, next) { - var Hapi = plugin.hapi; - - var handler = function (request, reply) { - - reply(Hapi.error.internal('Not implemented yet')); - }; - - plugin.route({ method: 'GET', path: '/', handler: handler }); + console.log(plugin.version); next(); }; ``` @@ -2514,6 +2461,21 @@ exports.register = function (plugin, options, next) { }; ``` +#### `plugin.path(path)` + +Sets the path prefix used to locate static resources (files and view templates) when relative paths are used by the plugin: +- `path` - the path prefix added to any relative file path starting with `'.'`. The value has the same effect as using the server's + configuration `files.relativeTo` option but only within the plugin. + +```javascript +exports.register = function (plugin, options, next) { + + plugin.path(__dirname + '../static'); + plugin.route({ path: '/file', method: 'GET', handler: { file: './test.html' } }); + next(); +}; +``` + #### `plugin.log(tags, [data, [timestamp]])` Emits a `'log'` event on the `pack.events` emitter using the same interface as [`server.log()`](#serverlogtags-data-timestamp). diff --git a/lib/pack.js b/lib/pack.js index 0844add16..dfd11f21f 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -305,13 +305,12 @@ internals.Pack.prototype._register = function (plugin, options, dependencies, ca var root = step(); - root.hapi = self.hapi; - root.version = plugin.version; + root.hapi = require('../'); + root.version = root.hapi.version; root.app = self.app; root.plugins = self.plugins; root.events = self.events; root.methods = self._methods.methods; - root.handlers = self._handlers; root.expose = function (/* key, value */) { diff --git a/test/pack.js b/test/pack.js index ecf187f5c..ccc2cb151 100755 --- a/test/pack.js +++ b/test/pack.js @@ -256,9 +256,10 @@ describe('Pack', function () { server.pack.register(plugin, function (err) { expect(err).to.not.exist; + expect(server.pack.list['--steve'].version).to.equal('0.0.0'); server.inject('/', function (res) { - expect(res.result).to.equal('0.0.0'); + expect(res.result).to.equal(Hapi.version); done(); }); }); From 5574cb777a9d8a18d6fc3d1d4cf5a6695e1c8fe3 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 26 May 2014 01:41:09 -0700 Subject: [PATCH 08/33] plugin vhost and path prefix override. Closes #981, Closes #1658, Closes #1659 --- docs/Reference.md | 57 +++------------------------------- lib/pack.js | 14 ++++++--- lib/route.js | 15 +++++++-- lib/router.js | 4 +-- lib/views.js | 6 +++- test/pack.js | 33 ++++++++++++++++++++ test/pack/--views/lib/index.js | 9 ++++-- 7 files changed, 73 insertions(+), 65 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index 3ac2510f0..93b04bcd9 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -2142,58 +2142,6 @@ pack.stop({ timeout: 60 * 1000 }, function () { }); ``` -#### `pack.require(name, [options], callback)` - -Registers a plugin where: - -- `name` - the node module name as expected by node's [`require()`](http://nodejs.org/api/modules.html#modules_module_require_id). If `name` is a relative - path it is relative to the location of the file requiring it. If `name` is not a relative or absolute path (e.g. 'furball'), it is prefixed with the - value of the pack `requirePath` configuration option when present. Note that node's `require()` is invoked by hapi which means, the `'node_modules'` path - is relative to the location of the hapi module. -- `options` - optional configuration object which is passed to the plugin via the `options` argument in - [`exports.register()`](#exportsregisterplugin-options-next). -- `callback` - the callback function with signature `function(err)` where: - - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts - (e.g. among routes, methods, state) will throw an error and will not return a callback. - -```javascript -pack.require('furball', { version: '/v' }, function (err) { - - if (err) { - console.log('Failed loading plugin: furball'); - } -}); -``` - -#### `pack.require(names, callback)` - -Registers a list of plugins where: - -- `names` - an array of plugins names as described in [`pack.register()`](#packregisterplugins-options-callback), or an object in which - each key is a plugin name, and each value is the `options` object used to register that plugin. -- `callback` - the callback function with signature `function(err)` where: - - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts - (e.g. among routes, methods, state) will throw an error and will not return a callback. - -Batch registration is required when plugins declare a [dependency](#plugindependencydeps-after), so that all the required dependencies are loaded in -a single transaction (internal order does not matter). - -```javascript -pack.require(['furball', 'lout'], function (err) { - - if (err) { - console.log('Failed loading plugin: furball'); - } -}); - -pack.require({ furball: null, lout: { endpoint: '/docs' } }, function (err) { - - if (err) { - console.log('Failed loading plugins'); - } -}); -``` - #### `pack.register(plugins, [options], callback)` Registers a plugin where: @@ -2201,7 +2149,10 @@ Registers a plugin where: - `plugins` - a plugin object or array of plugin objects. The objects can use one of two formats: - a module plugin object. - a manually constructed plugin object. -- `options` - optional registration options (used by **hapi** and is not passed to the plugin). +- `options` - optional registration options (used by **hapi** and is not passed to the plugin): + - `route` - apply modifiers to any routes added by the plugin: + - `prefix` - string added as prefix to any route path (must begin with `'/'`). + - `vhost` - virtual host value set to every route. - `callback` - the callback function with signature `function(err)` where: - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts (e.g. among routes, methods, state) will throw an error and will not return a callback. diff --git a/lib/pack.js b/lib/pack.js index dfd11f21f..536a4982e 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -166,7 +166,7 @@ internals.Pack.prototype.register = function (plugins /*, [options], callback */ var self = this; plugins = [].concat(plugins); - var options = (arguments.length === 3 ? arguments[1] : null); + var options = (arguments.length === 3 ? arguments[1] : {}); var callback = (arguments.length === 3 ? arguments[2] : arguments[1]); /* @@ -213,7 +213,7 @@ internals.Pack.prototype.register = function (plugins /*, [options], callback */ var dependencies = {}; Async.forEachSeries(registrations, function (item, next) { - self._register(item, {}, dependencies, next); + self._register(item, options, dependencies, next); }, function (err) { @@ -244,8 +244,12 @@ internals.Pack.prototype._register = function (plugin, options, dependencies, ca var env = { name: plugin.name, + path: null, bind: null, - path: null + route: { + prefix: options.route && options.route.prefix, + vhost: options.route && options.route.vhost + } }; this._env[plugin.name] = env; @@ -366,8 +370,8 @@ internals.Pack.prototype._register = function (plugin, options, dependencies, ca root.views = function (options) { Hoek.assert(!env.views, 'Cannot set plugin views manager more than once'); - options.basePath = options.basePath || env.path; - env.views = new Views.Manager(options); + var override = (!options.basePath && env.path ? { basePath: env.path } : null); + env.views = new Views.Manager(options, override); }; root.method = function (/* name, method, options */) { diff --git a/lib/route.js b/lib/route.js index 83fa2ff7f..3f7af8a75 100755 --- a/lib/route.js +++ b/lib/route.js @@ -21,11 +21,21 @@ exports = module.exports = internals.Route = function (options, server, env) { var self = this; + // Apply plugin environment + + if (env && + (env.route.vhost || env.route.prefix)) { + + options = Hoek.clone(options); + options.path = (env.route.prefix || '') + options.path; + options.vhost = env.route.vhost || options.vhost; + } + // Setup and validate route configuration Hoek.assert(options.handler || (options.config && options.config.handler), 'Missing or undefined handler:', options.path); Hoek.assert(!!options.handler ^ !!(options.config && options.config.handler), 'Handler must only appear once:', options.path); // XOR - Hoek.assert(options.path.match(internals.Route.pathRegex.validatePath), 'Invalid path:', options.path); + Hoek.assert(internals.Route.pathRegex.validatePath.test(options.path), 'Invalid path:', options.path); Hoek.assert(options.path.match(internals.Route.pathRegex.validatePathEncoded) === null, 'Path cannot contain encoded non-reserved path characters:', options.path); Hoek.assert(options.path === '/' || options.path[options.path.length - 1] !== '/' || !server.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when server configured to strip:', options.path); @@ -36,12 +46,13 @@ exports = module.exports = internals.Route = function (options, server, env) { Schema.assert('routeConfig', this.settings, options.path); this.server = server; - this.env = env || {}; // Plugin-specific environment this.method = options.method.toLowerCase(); this.settings.method = this.method; // Expose method in settings this.settings.path = options.path; // Expose path in settings + this.settings.vhost = options.vhost; // Expose vhost in settings this.settings.plugins = this.settings.plugins || {}; // Route-specific plugins settings, namespaced using plugin name this.settings.app = this.settings.app || {}; // Route-specific application settings + this.env = env || {}; // Plugin-specific environment // Path parsing diff --git a/lib/router.js b/lib/router.js index 000cc3bb2..eddb838a8 100755 --- a/lib/router.js +++ b/lib/router.js @@ -107,8 +107,8 @@ internals.Router.prototype.add = function (configs, env) { methods.forEach(function (method) { config.method = method; - var route = new Route(config, self.server, env); // Do no use config beyond this point, use route members - var vhosts = [].concat(config.vhost || '*'); + var route = new Route(config, self.server, env); // Do no use config beyond this point, use route members + var vhosts = [].concat(route.settings.vhost || '*'); vhosts.forEach(function (vhost) { diff --git a/lib/views.js b/lib/views.js index a1ace2298..dc46dfb00 100755 --- a/lib/views.js +++ b/lib/views.js @@ -18,7 +18,7 @@ var internals = {}; // View Manager -exports.Manager = internals.Manager = function (options) { +exports.Manager = internals.Manager = function (options, override) { var self = this; @@ -26,6 +26,10 @@ exports.Manager = internals.Manager = function (options) { Hoek.assert(extensions.length, 'Views manager requires at least one registered extension handler'); var defaults = Hoek.applyToDefaults(Defaults.views, options); + if (override) { + var defaults = Hoek.applyToDefaults(defaults, override); + } + delete defaults.engines; delete defaults.defaultExtension; diff --git a/test/pack.js b/test/pack.js index ccc2cb151..987ed3378 100755 --- a/test/pack.js +++ b/test/pack.js @@ -459,6 +459,39 @@ describe('Pack', function () { }); }); + it('registers a plugin with route path prefix', function (done) { + + var server = new Hapi.Server({ labels: 'test' }); + server.pack.register(require('./pack/--test1'), { route: { prefix: '/xyz' } }, function (err) { + + expect(err).to.not.exist; + server.inject('/xyz/test1', function (res) { + + expect(res.result).to.equal('testing123'); + done(); + }); + }); + }); + + it('registers a plugin with route vhost', function (done) { + + var server = new Hapi.Server({ labels: 'test' }); + server.pack.register(require('./pack/--test1'), { route: { vhost: 'example.com' } }, function (err) { + + expect(err).to.not.exist; + server.inject('/test1', function (res) { + + expect(res.statusCode).to.equal(404); + + server.inject({ url: '/test1', headers: { host: 'example.com' } }, function (res) { + + expect(res.result).to.equal('testing123'); + done(); + }); + }); + }); + }); + it('fails to require missing module', function (done) { var pack = new Hapi.Pack(); diff --git a/test/pack/--views/lib/index.js b/test/pack/--views/lib/index.js index 115fb765d..7d180e715 100755 --- a/test/pack/--views/lib/index.js +++ b/test/pack/--views/lib/index.js @@ -14,10 +14,15 @@ exports.register = function (plugin, options, next) { plugin.path(Path.join(__dirname, '..')); - plugin.views({ + var views = { engines: { 'html': require('handlebars') }, path: './templates' - }); + }; + + plugin.views(views); + if (Object.keys(views).length !== 2) { + return next(new Error('plugin.view() modified options')); + } plugin.route([ { From bb0fcb5ca47cc949b8adbf7af94921e4657494f1 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 26 May 2014 10:01:26 -0700 Subject: [PATCH 09/33] Prevent view manager from cloning engines. Closes #1661 --- lib/views.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/views.js b/lib/views.js index dc46dfb00..6f3f22f52 100755 --- a/lib/views.js +++ b/lib/views.js @@ -22,25 +22,35 @@ exports.Manager = internals.Manager = function (options, override) { var self = this; - var extensions = Object.keys(options.engines); - Hoek.assert(extensions.length, 'Views manager requires at least one registered extension handler'); + // Save non-defaults values + + var engines = options.engines; + var defaultExtension = options.defaultExtension; + + // Clone options + options.engines = null; // Prevent engines from being cloned var defaults = Hoek.applyToDefaults(Defaults.views, options); if (override) { var defaults = Hoek.applyToDefaults(defaults, override); } - delete defaults.engines; + options.engines = engines; // Restore object to original state delete defaults.defaultExtension; + // Prepare manager state + + var extensions = Object.keys(engines); + Hoek.assert(extensions.length, 'Views manager requires at least one registered extension handler'); + this._engines = {}; - this._defaultExtension = options.defaultExtension || (extensions.length === 1 ? extensions[0] : ''); + this._defaultExtension = defaultExtension || (extensions.length === 1 ? extensions[0] : ''); // Load engines extensions.forEach(function (extension) { - var config = options.engines[extension]; + var config = engines[extension]; var engine = {}; if (config.compile && From 9b09ec573a08b4622722e7626f5a23a9c8cc853d Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 26 May 2014 12:16:50 -0700 Subject: [PATCH 10/33] Prevent clone from deep copy of unsafe object. Closes #1662 --- lib/methods.js | 3 ++- lib/route.js | 7 ++++--- lib/schema.js | 6 +++--- lib/utils.js | 38 ++++++++++++++++++++++++++++++++++++++ lib/views.js | 10 ++++------ test/methods.js | 27 +++++++++++++++++++++++++++ test/route.js | 36 ++++++++++++++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 13 deletions(-) diff --git a/lib/methods.js b/lib/methods.js index 23ab3cab5..4b19f8822 100755 --- a/lib/methods.js +++ b/lib/methods.js @@ -3,6 +3,7 @@ var Boom = require('boom'); var Hoek = require('hoek'); var Schema = require('./schema'); +var Utils = require('./utils'); // Declare internals @@ -47,7 +48,7 @@ internals.Methods.prototype._add = function (name, fn, options, env) { options = options || {}; Schema.assert('method', options, name); - var settings = Hoek.clone(options); + var settings = Utils.cloneWithShallow(options, ['bind']); settings.generateKey = settings.generateKey || internals.generateKey; var bind = settings.bind || (env && env.bind) || null; diff --git a/lib/route.js b/lib/route.js index 3f7af8a75..9df686a67 100755 --- a/lib/route.js +++ b/lib/route.js @@ -10,6 +10,7 @@ var Auth = require('./auth'); var Payload = require('./payload'); var Validation = require('./validation'); var Handler = require('./handler'); +var Utils = require('./utils'); // Declare internals @@ -21,12 +22,12 @@ exports = module.exports = internals.Route = function (options, server, env) { var self = this; - // Apply plugin environment + // Apply plugin environment (before schema validation) if (env && (env.route.vhost || env.route.prefix)) { - options = Hoek.clone(options); + options = Utils.cloneWithShallow(options, ['config']); options.path = (env.route.prefix || '') + options.path; options.vhost = env.route.vhost || options.vhost; } @@ -39,7 +40,7 @@ exports = module.exports = internals.Route = function (options, server, env) { Hoek.assert(options.path.match(internals.Route.pathRegex.validatePathEncoded) === null, 'Path cannot contain encoded non-reserved path characters:', options.path); Hoek.assert(options.path === '/' || options.path[options.path.length - 1] !== '/' || !server.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when server configured to strip:', options.path); - this.settings = Hoek.clone(options.config) || {}; + this.settings = Utils.cloneWithShallow(options.config, ['bind', 'plugins', 'app']) || {}; this.settings.handler = this.settings.handler || options.handler; Schema.assert('route', options, options.path); diff --git a/lib/schema.js b/lib/schema.js index bebb6ea11..5231bec04 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -149,7 +149,7 @@ internals.route = Joi.object({ path: Joi.string().required(), vhost: [ Joi.string(), - Joi.array() + Joi.array().includes(Joi.string()).min(1) ], handler: Joi.any(), // Validated in route.config config: Joi.object().allow(null) @@ -238,11 +238,11 @@ internals.routeConfig = Joi.object({ description: Joi.string(), notes: [ Joi.string(), - Joi.array() + Joi.array().includes(Joi.string()) ], tags: [ Joi.string(), - Joi.array() + Joi.array().includes(Joi.string()) ] }); diff --git a/lib/utils.js b/lib/utils.js index 5229cf2bf..426b62f25 100755 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,6 +2,7 @@ var Crypto = require('crypto'); var Path = require('path'); +var Hoek = require('hoek'); // Declare internals @@ -25,3 +26,40 @@ exports.uniqueFilename = function (path) { var name = [Date.now(), process.pid, Crypto.randomBytes(8).toString('hex')].join('-'); return Path.join(path, name); }; + + +exports.cloneWithShallow = function (source, keys) { + + if (!source || + typeof source !== 'object') { + + return source; + } + + // Move shallow copy items to storage + + var storage = {}; + for (var i = 0, il = keys.length; i < il; ++i) { + var key = keys[i]; + if (source.hasOwnProperty(key)) { + storage[key] = source[key]; + source[key] = null; + } + } + + // Deep copy the rest + + var copy = Hoek.clone(source); + + // Shallow copy the stored items + + for (i = 0; i < il; ++i) { + var key = keys[i]; + if (storage.hasOwnProperty(key)) { + source[key] = storage[key]; + copy[key] = storage[key]; + } + } + + return copy; +}; \ No newline at end of file diff --git a/lib/views.js b/lib/views.js index 6f3f22f52..19ec8541d 100755 --- a/lib/views.js +++ b/lib/views.js @@ -7,8 +7,7 @@ var Hoek = require('hoek'); var Defaults = require('./defaults'); var Schema = require('./schema'); var Response = require('./response'); -var Schema = require('./schema'); -// Additional engine modules required in constructor +// Additional helper modules required in constructor // Declare internals @@ -268,17 +267,16 @@ internals.Manager.prototype.render = function (filename, context, options, callb return callback(err); } - var layoutContext = Hoek.clone(context); - compiled(context, settings.runtimeOptions, function (err, rendered) { if (err) { return callback(Boom.badImplementation(err.message, err)); } - layoutContext[settings.layoutKeyword] = rendered; + context[settings.layoutKeyword] = rendered; + layout(context, settings.runtimeOptions, function (err, rendered) { - layout(layoutContext, settings.runtimeOptions, function (err, rendered) { + delete context[settings.layoutKeyword]; if (err) { return callback(Boom.badImplementation(err.message, err)); diff --git a/test/methods.js b/test/methods.js index f8ef5978c..dd89b8774 100755 --- a/test/methods.js +++ b/test/methods.js @@ -597,4 +597,31 @@ describe('Method', function () { }); }); }); + + it('shallow copies bind config', function (done) { + + var bind = { gen: 7 }; + var method = function (id, next) { + + return next(null, { id: id, gen: this.gen++, bound: (this === bind) }); + }; + + var server = new Hapi.Server(0); + server.method('test', method, { bind: bind, cache: { expiresIn: 1000 } }); + + server.start(function () { + + server.methods.test(1, function (err, result) { + + expect(result.gen).to.equal(7); + expect(result.bound).to.equal(true); + + server.methods.test(1, function (err, result) { + + expect(result.gen).to.equal(7); + done(); + }); + }); + }); + }); }); diff --git a/test/route.js b/test/route.js index 5c0c6f2e0..04c9eca69 100755 --- a/test/route.js +++ b/test/route.js @@ -76,6 +76,42 @@ describe('Route', function () { done(); }); + it('shallow copies route config bind', function (done) { + + var server = new Hapi.Server(); + var context = { key: 'is ' }; + server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(this.key + (this === context)); }, config: { bind: context } }); + server.inject('/', function (res) { + + expect(res.result).to.equal('is true'); + done(); + }); + }); + + it('shallow copies route config app', function (done) { + + var server = new Hapi.Server(); + var app = { key: 'is ' }; + server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(request.route.app.key + (request.route.app === app)); }, config: { app: app } }); + server.inject('/', function (res) { + + expect(res.result).to.equal('is true'); + done(); + }); + }); + + it('shallow copies route config plugins', function (done) { + + var server = new Hapi.Server(); + var plugins = { key: 'is ' }; + server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(request.route.plugins.key + (request.route.plugins === plugins)); }, config: { plugins: plugins } }); + server.inject('/', function (res) { + + expect(res.result).to.equal('is true'); + done(); + }); + }); + describe('#sort', function () { var paths = [ From dc3fd910c2ba9ac57dbafdaafa3418a0f4a5ff51 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 26 May 2014 14:07:25 -0700 Subject: [PATCH 11/33] Server config clone. For #1662 --- lib/server.js | 3 ++- lib/utils.js | 22 ++++++++++++++++++++-- lib/views.js | 12 +++++------- test/server.js | 24 ++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/lib/server.js b/lib/server.js index b6043257c..85494f714 100755 --- a/lib/server.js +++ b/lib/server.js @@ -16,6 +16,7 @@ var Router = require('./router'); var Schema = require('./schema'); var Views = require('./views'); var Ext = require('./ext'); +var Utils = require('./utils'); // Pack delayed required inline @@ -69,7 +70,7 @@ exports = module.exports = internals.Server = function (/* host, port, options * args[key] = argVal; } - this.settings = Hoek.applyToDefaults(Defaults.server, args.options || {}); + this.settings = Utils.applyToDefaultsWithShallow(Defaults.server, args.options || {}, ['app', 'plugins', 'views']); Schema.assert('server', this.settings); this.settings.labels = Hoek.unique([].concat(this.settings.labels)); // Convert string to array and removes duplicates diff --git a/lib/utils.js b/lib/utils.js index 426b62f25..1f0e9eb7d 100755 --- a/lib/utils.js +++ b/lib/utils.js @@ -36,6 +36,24 @@ exports.cloneWithShallow = function (source, keys) { return source; } + return internals.shallow(source, keys, function () { + + return Hoek.clone(source); + }); +}; + + +exports.applyToDefaultsWithShallow = function (defaults, options, keys) { + + return internals.shallow(options, keys, function () { + + return Hoek.applyToDefaults(defaults, options); + }); +}; + + +internals.shallow = function (source, keys, clone) { + // Move shallow copy items to storage var storage = {}; @@ -43,13 +61,13 @@ exports.cloneWithShallow = function (source, keys) { var key = keys[i]; if (source.hasOwnProperty(key)) { storage[key] = source[key]; - source[key] = null; + source[key] = undefined; } } // Deep copy the rest - var copy = Hoek.clone(source); + var copy = clone(); // Shallow copy the stored items diff --git a/lib/views.js b/lib/views.js index 19ec8541d..831275370 100755 --- a/lib/views.js +++ b/lib/views.js @@ -7,6 +7,7 @@ var Hoek = require('hoek'); var Defaults = require('./defaults'); var Schema = require('./schema'); var Response = require('./response'); +var Utils = require('./utils'); // Additional helper modules required in constructor @@ -28,13 +29,12 @@ exports.Manager = internals.Manager = function (options, override) { // Clone options - options.engines = null; // Prevent engines from being cloned - var defaults = Hoek.applyToDefaults(Defaults.views, options); + var defaults = Utils.applyToDefaultsWithShallow(Defaults.views, options, ['engines']); if (override) { - var defaults = Hoek.applyToDefaults(defaults, override); + Hoek.merge(defaults, override); } - options.engines = engines; // Restore object to original state + delete defaults.engines; delete defaults.defaultExtension; // Prepare manager state @@ -62,9 +62,7 @@ exports.Manager = internals.Manager = function (options, override) { Schema.assert('view', config); engine.module = config.module; - config.module = null; // Prevent module from being cloned - engine.config = Hoek.applyToDefaults(defaults, config); - config.module = engine.module; // Restore object to original state + engine.config = Utils.applyToDefaultsWithShallow(defaults, config, ['module']); } engine.suffix = '.' + extension; diff --git a/test/server.js b/test/server.js index 31a0d3980..626ad33de 100755 --- a/test/server.js +++ b/test/server.js @@ -30,6 +30,30 @@ var it = Lab.test; describe('Server', function () { + it('shallow clones app config', function (done) { + + var item = {}; + var server = new Hapi.Server({ app: item }); + expect(server.settings.app).to.equal(item); + done(); + }); + + it('shallow clones plugins config', function (done) { + + var item = {}; + var server = new Hapi.Server({ plugins: item }); + expect(server.settings.plugins).to.equal(item); + done(); + }); + + it('shallow clones views config', function (done) { + + var views = { engines: { html: require('handlebars') } }; + var server = new Hapi.Server({ views: views }); + expect(server.settings.views).to.equal(views); + done(); + }); + it('calls start twice', function (done) { var server = new Hapi.Server(0); From 474fed55e0641b83abf59b516b50d6c28e01b2ed Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 26 May 2014 14:31:22 -0700 Subject: [PATCH 12/33] Apply plugin register route options to child register. For #1658 --- lib/pack.js | 16 ++++++++-- test/pack.js | 56 ++++++++++++++++++++++++++++++++++ test/pack/--child/lib/index.js | 8 ++++- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/lib/pack.js b/lib/pack.js index 536a4982e..df30d609b 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -327,9 +327,21 @@ internals.Pack.prototype._register = function (plugin, options, dependencies, ca self.log(tags, data, timestamp); }; - root.register = function () { + root.register = function (plugins /*, [options], callback */) { - internals.Pack.prototype.register.apply(self, arguments); + var options = (arguments.length === 3 ? arguments[1] : {}); + var callback = (arguments.length === 3 ? arguments[2] : arguments[1]); + + if (env.route.prefix || + env.route.vhost) { + + options = Hoek.clone(options); + options.route = options.route || {}; + options.route.prefix = (env.route.prefix || '') + (options.route.prefix || ''); + options.route.vhost = options.route.vhost || env.route.vhost; + } + + internals.Pack.prototype.register.call(self, plugins, options, callback); }; root.dependency = function (deps, after) { diff --git a/test/pack.js b/test/pack.js index 987ed3378..59be11bb5 100755 --- a/test/pack.js +++ b/test/pack.js @@ -473,6 +473,62 @@ describe('Pack', function () { }); }); + it('registers a child plugin with parent route path prefix', function (done) { + + var server = new Hapi.Server({ labels: 'test' }); + server.pack.register(require('./pack/--child'), { route: { prefix: '/xyz' } }, function (err) { + + expect(err).to.not.exist; + server.inject('/xyz/test1', function (res) { + + expect(res.result).to.equal('testing123'); + done(); + }); + }); + }); + + it('registers a child plugin with parent route vhost prefix', function (done) { + + var server = new Hapi.Server({ labels: 'test' }); + server.pack.register(require('./pack/--child'), { route: { vhost: 'example.com' } }, function (err) { + + expect(err).to.not.exist; + server.inject({ url: '/test1', headers: { host: 'example.com' } }, function (res) { + + expect(res.result).to.equal('testing123'); + done(); + }); + }); + }); + + it('registers a child plugin with parent route path prefix and inner register prefix', function (done) { + + var server = new Hapi.Server({ labels: 'test' }); + server.pack.register({ plugin: require('./pack/--child'), options: { route: { prefix: '/inner' } } }, { route: { prefix: '/xyz' } }, function (err) { + + expect(err).to.not.exist; + server.inject('/xyz/inner/test1', function (res) { + + expect(res.result).to.equal('testing123'); + done(); + }); + }); + }); + + it('registers a child plugin with parent route vhost prefix and inner register vhost', function (done) { + + var server = new Hapi.Server({ labels: 'test' }); + server.pack.register({ plugin: require('./pack/--child'), options: { route: { vhost: 'example.net' } } }, { route: { vhost: 'example.com' } }, function (err) { + + expect(err).to.not.exist; + server.inject({ url: '/test1', headers: { host: 'example.net' } }, function (res) { + + expect(res.result).to.equal('testing123'); + done(); + }); + }); + }); + it('registers a plugin with route vhost', function (done) { var server = new Hapi.Server({ labels: 'test' }); diff --git a/test/pack/--child/lib/index.js b/test/pack/--child/lib/index.js index 139772845..ccce08dc1 100755 --- a/test/pack/--child/lib/index.js +++ b/test/pack/--child/lib/index.js @@ -7,7 +7,13 @@ var internals = {}; exports.register = function (plugin, options, next) { - plugin.register(require('../../--test1'), {}, next); + if (options) { + + plugin.register(require('../../--test1'), options, next); + } + else { + plugin.register(require('../../--test1'), next); + } }; From fabebeeb0a1914d1b94b585640aa7faed7a5f200 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 26 May 2014 15:09:37 -0700 Subject: [PATCH 13/33] Pre-select labels in register. Closes #1663 --- docs/Reference.md | 7 +++-- lib/pack.js | 11 ++++--- lib/schema.js | 31 +++++++++++++++----- test/pack.js | 74 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 108 insertions(+), 15 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index 93b04bcd9..ae698edde 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -2150,9 +2150,12 @@ Registers a plugin where: - a module plugin object. - a manually constructed plugin object. - `options` - optional registration options (used by **hapi** and is not passed to the plugin): + - `labels` - string or array of strings of labels or pre-select for plugin registration. - `route` - apply modifiers to any routes added by the plugin: - - `prefix` - string added as prefix to any route path (must begin with `'/'`). - - `vhost` - virtual host value set to every route. + - `prefix` - string added as prefix to any route path (must begin with `'/'`). If a plugin registers a child plugin + the `prefix` is passed on to the child or is added in front of the child-specific prefix. + - `vhost` - virtual host string (or array of strings) applied to every route. The outter-most `vhost` overrides the any + nested configuration. - `callback` - the callback function with signature `function(err)` where: - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts (e.g. among routes, methods, state) will throw an error and will not return a callback. diff --git a/lib/pack.js b/lib/pack.js index df30d609b..e08662c78 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -11,6 +11,7 @@ var Defaults = require('./defaults'); var Ext = require('./ext'); var Methods = require('./methods'); var Handler = require('./handler'); +var Schema = require('./schema'); var Utils = require('./utils'); @@ -239,6 +240,7 @@ internals.Pack.prototype._register = function (plugin, options, dependencies, ca var self = this; Hoek.assert(!this._env[plugin.name], 'Plugin already registered:', plugin.name); + //Schema.assert('register', options); // Setup environment @@ -307,7 +309,7 @@ internals.Pack.prototype._register = function (plugin, options, dependencies, ca // Setup root pack object - var root = step(); + var root = step(options.labels); root.hapi = require('../'); root.version = root.hapi.version; @@ -337,8 +339,8 @@ internals.Pack.prototype._register = function (plugin, options, dependencies, ca options = Hoek.clone(options); options.route = options.route || {}; - options.route.prefix = (env.route.prefix || '') + (options.route.prefix || ''); - options.route.vhost = options.route.vhost || env.route.vhost; + options.route.prefix = (env.route.prefix || '') + (options.route.prefix || '') || undefined; + options.route.vhost = env.route.vhost || options.route.vhost; } internals.Pack.prototype.register.call(self, plugins, options, callback); @@ -438,7 +440,8 @@ internals.Pack.prototype._select = function (labels, subset) { var self = this; - Hoek.assert(!labels || Array.isArray(labels), 'Bad labels object type (undefined or array required)'); + Hoek.assert(!labels || typeof labels === 'string' || Array.isArray(labels), 'Bad labels object type (undefined or array required)'); + labels = labels && [].concat(labels); var ids = []; if (labels) { diff --git a/lib/schema.js b/lib/schema.js index 5231bec04..28f5b90b4 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -73,6 +73,12 @@ internals.security = Joi.object({ }).allow(null, false, true); +internals.labels = Joi.alternatives([ + Joi.string(), + Joi.array().includes(Joi.string()) +]); + + internals.server = Joi.object({ app: Joi.object().allow(null), cache: Joi.alternatives(Joi.string(), internals.cache, Joi.array().includes(internals.cache)).allow(null), @@ -102,10 +108,7 @@ internals.server = Joi.object({ space: Joi.number().allow(null), suffix: Joi.string().allow(null) }), - labels: [ - Joi.string(), - Joi.array().includes(Joi.string()) - ], + labels: internals.labels, load: { maxHeapUsedBytes: Joi.number().min(0), maxEventLoopDelay: Joi.number().min(0), @@ -144,13 +147,16 @@ internals.server = Joi.object({ }); +internals.vhost = Joi.alternatives([ + Joi.string().hostname(), + Joi.array().includes(Joi.string().hostname()).min(1) +]); + + internals.route = Joi.object({ method: Joi.alternatives(Joi.string(), Joi.array().includes(Joi.string()).min(1)).required(), path: Joi.string().required(), - vhost: [ - Joi.string(), - Joi.array().includes(Joi.string()).min(1) - ], + vhost: internals.vhost, handler: Joi.any(), // Validated in route.config config: Joi.object().allow(null) }); @@ -326,3 +332,12 @@ internals.method = Joi.object({ generateKey: Joi.func(), cache: internals.cachePolicy }); + + +internals.register = Joi.object({ + route: Joi.object({ + prefix: Joi.string().regex(/^\/.+/), + vhost: internals.vhost + }), + labels: internals.labels +}); \ No newline at end of file diff --git a/test/pack.js b/test/pack.js index 59be11bb5..b7230fc7b 100755 --- a/test/pack.js +++ b/test/pack.js @@ -521,7 +521,7 @@ describe('Pack', function () { server.pack.register({ plugin: require('./pack/--child'), options: { route: { vhost: 'example.net' } } }, { route: { vhost: 'example.com' } }, function (err) { expect(err).to.not.exist; - server.inject({ url: '/test1', headers: { host: 'example.net' } }, function (res) { + server.inject({ url: '/test1', headers: { host: 'example.com' } }, function (res) { expect(res.result).to.equal('testing123'); done(); @@ -1183,6 +1183,78 @@ describe('Pack', function () { }); }); + it('registers plugins with pre-selected label', function (done) { + + var pack = new Hapi.Pack(); + pack.server({ labels: ['a'] }); + pack.server({ labels: ['b'] }); + + var server1 = pack._servers[0]; + var server2 = pack._servers[1]; + + var plugin = { + name: 'test', + register: function (plugin, options, next) { + + plugin.route({ method: 'GET', path: '/', handler: function (request, reply) { reply('ok'); } }); + next(); + } + }; + + pack.register(plugin, { labels: 'a' }, function (err) { + + expect(err).to.not.exist; + server1.inject('/', function (res) { + + expect(res.statusCode).to.equal(200); + server2.inject('/', function (res) { + + expect(res.statusCode).to.equal(404); + done(); + }); + }); + }); + }); + + it('registers plugins with pre-selected labels', function (done) { + + var pack = new Hapi.Pack(); + pack.server({ labels: ['a'] }); + pack.server({ labels: ['b'] }); + pack.server({ labels: ['c'] }); + + var server1 = pack._servers[0]; + var server2 = pack._servers[1]; + var server3 = pack._servers[2]; + + var plugin = { + name: 'test', + register: function (plugin, options, next) { + + plugin.route({ method: 'GET', path: '/', handler: function (request, reply) { reply('ok'); } }); + next(); + } + }; + + pack.register(plugin, { labels: ['a', 'c'] }, function (err) { + + expect(err).to.not.exist; + server1.inject('/', function (res) { + + expect(res.statusCode).to.equal(200); + server2.inject('/', function (res) { + + expect(res.statusCode).to.equal(404); + server3.inject('/', function (res) { + + expect(res.statusCode).to.equal(200); + done(); + }); + }); + }); + }); + }); + describe('#_provisionCache ', function () { it('throws when missing options', function (done) { From 3054058271966690a49c6a357297f271dbb152f6 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 26 May 2014 19:32:16 -0700 Subject: [PATCH 14/33] Cleanup --- lib/pack.js | 12 ++++++------ test/pack/--child/lib/index.js | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/pack.js b/lib/pack.js index e08662c78..84b80d8fe 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -235,12 +235,12 @@ internals.Pack.prototype.register = function (plugins /*, [options], callback */ }; -internals.Pack.prototype._register = function (plugin, options, dependencies, callback) { +internals.Pack.prototype._register = function (plugin, registerOptions, dependencies, callback) { var self = this; Hoek.assert(!this._env[plugin.name], 'Plugin already registered:', plugin.name); - //Schema.assert('register', options); + Schema.assert('register', registerOptions); // Setup environment @@ -249,8 +249,8 @@ internals.Pack.prototype._register = function (plugin, options, dependencies, ca path: null, bind: null, route: { - prefix: options.route && options.route.prefix, - vhost: options.route && options.route.vhost + prefix: registerOptions.route && registerOptions.route.prefix, + vhost: registerOptions.route && registerOptions.route.vhost } }; @@ -309,7 +309,7 @@ internals.Pack.prototype._register = function (plugin, options, dependencies, ca // Setup root pack object - var root = step(options.labels); + var root = step(registerOptions.labels); root.hapi = require('../'); root.version = root.hapi.version; @@ -414,7 +414,7 @@ internals.Pack.prototype._register = function (plugin, options, dependencies, ca // Register - plugin.register.call(null, root, plugin.options, callback); + plugin.register.call(null, root, plugin.options || {}, callback); }; diff --git a/test/pack/--child/lib/index.js b/test/pack/--child/lib/index.js index ccce08dc1..6e0174f9e 100755 --- a/test/pack/--child/lib/index.js +++ b/test/pack/--child/lib/index.js @@ -7,8 +7,7 @@ var internals = {}; exports.register = function (plugin, options, next) { - if (options) { - + if (options.route) { plugin.register(require('../../--test1'), options, next); } else { From 54bdf0551fef99fc24469b58a5de150485fd89a4 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Thu, 29 May 2014 07:45:26 -0700 Subject: [PATCH 15/33] Divider object --- lib/divider.js | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/pack.js | 12 ++---- 2 files changed, 110 insertions(+), 9 deletions(-) create mode 100755 lib/divider.js diff --git a/lib/divider.js b/lib/divider.js new file mode 100755 index 000000000..24afe2d78 --- /dev/null +++ b/lib/divider.js @@ -0,0 +1,107 @@ +// Load modules + +var Events = require('events'); +var Hoek = require('hoek'); + + +// Declare internals + +var internals = {}; + + +exports = module.exports = internals.Divider = function () { + + this._sources = []; + this._handlers = {}; + + Events.EventEmitter.call(this); +}; + +Hoek.inherits(internals.Divider, Events.EventEmitter); + + +internals.Divider.prototype.addEmitter = function (emitter) { + + var self = this; + + this._sources.push(emitter); + var types = Object.keys(this._events); + for (var i = 0, il = types.length; i < il; ++i) { + var type = types[i]; + emitter.on(type, this._handler(type)); + } +}; + + +internals.Divider.prototype._handler = function (type) { + + var self = this; + + if (this._handlers[type]) { + return this._handlers[type]; + } + + var handler = function (arg1, arg2, arg3, arg4, arg5) { + + self.emit(type, arg1, arg2, arg3, arg4, arg5); + self._unsubscribe(type); + }; + + this._handlers[type] = handler; + return handler; +}; + + +internals.Divider.prototype.on = internals.Divider.prototype.addListener = function (type, listener) { + + this._subscribe(type); + return Events.EventEmitter.prototype.on.apply(this, arguments); +}; + + +internals.Divider.prototype.once = function (type, listener) { + + this._subscribe(type); + return Events.EventEmitter.prototype.once.apply(this, arguments); +}; + + +internals.Divider.prototype._subscribe = function (type) { + + if (!this._events[type]) { + for (var i = 0, il = this._sources.length; i < il; ++i) { + this._sources[i].on(type, this._handler(type)); + } + } +}; + + +internals.Divider.prototype.removeListener = function (type, listener) { + + Events.EventEmitter.prototype.removeListener.apply(this, arguments); + this._unsubscribe(type); + return this; +}; + + +internals.Divider.prototype.removeAllListeners = function (type) { + + Events.EventEmitter.prototype.removeAllListeners.apply(this, arguments); + this._unsubscribe(type); + return this; +}; + + +internals.Divider.prototype._unsubscribe = function (type) { + + if (!this._events[type]) { + for (var i = 0, il = this._sources.length; i < il; ++i) { + this._sources[i].removeListener(type, this._handler(type)); + } + } + else if (type === undefined) { + for (var i = 0, il = this._sources.length; i < il; ++i) { + this._sources[i].removeAllListeners(); + } + } +}; \ No newline at end of file diff --git a/lib/pack.js b/lib/pack.js index 84b80d8fe..69406f257 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -1,10 +1,10 @@ // Load modules var Path = require('path'); -var Events = require('events'); var Async = require('async'); var Catbox = require('catbox'); var Hoek = require('hoek'); +var Divider = require('./divider'); var Server = require('./server'); var Views = require('./views'); var Defaults = require('./defaults'); @@ -37,7 +37,7 @@ exports = module.exports = internals.Pack = function (options) { this.list = {}; // Loaded plugins by name this.plugins = {}; // Exposed plugin properties by name - this.events = new Events.EventEmitter(); // Consolidated subscription to all servers' events + this.events = new Divider(); // Consolidated subscription to all servers this.app = options.app || {}; if (options.cache) { @@ -121,13 +121,7 @@ internals.Pack.prototype._server = function (server) { // Subscribe to events - ['request', 'response', 'tail', 'internalError'].forEach(function (event) { - - server.on(event, function (request, data, tags) { - - self.events.emit(event, request, data, tags); - }); - }); + this.events.addEmitter(server); return server; }; From cf298c13917b02ee2117ba8cd444f7ec7845feb9 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Thu, 29 May 2014 12:36:35 -0700 Subject: [PATCH 16/33] Move shallow utils to hoek --- lib/methods.js | 3 +-- lib/route.js | 5 ++--- lib/server.js | 3 +-- lib/utils.js | 55 -------------------------------------------------- lib/views.js | 11 +++++----- package.json | 2 +- 6 files changed, 10 insertions(+), 69 deletions(-) diff --git a/lib/methods.js b/lib/methods.js index 4b19f8822..377ab0018 100755 --- a/lib/methods.js +++ b/lib/methods.js @@ -3,7 +3,6 @@ var Boom = require('boom'); var Hoek = require('hoek'); var Schema = require('./schema'); -var Utils = require('./utils'); // Declare internals @@ -48,7 +47,7 @@ internals.Methods.prototype._add = function (name, fn, options, env) { options = options || {}; Schema.assert('method', options, name); - var settings = Utils.cloneWithShallow(options, ['bind']); + var settings = Hoek.cloneWithShallow(options, ['bind']); settings.generateKey = settings.generateKey || internals.generateKey; var bind = settings.bind || (env && env.bind) || null; diff --git a/lib/route.js b/lib/route.js index 9df686a67..7a82bc525 100755 --- a/lib/route.js +++ b/lib/route.js @@ -10,7 +10,6 @@ var Auth = require('./auth'); var Payload = require('./payload'); var Validation = require('./validation'); var Handler = require('./handler'); -var Utils = require('./utils'); // Declare internals @@ -27,7 +26,7 @@ exports = module.exports = internals.Route = function (options, server, env) { if (env && (env.route.vhost || env.route.prefix)) { - options = Utils.cloneWithShallow(options, ['config']); + options = Hoek.cloneWithShallow(options, ['config']); options.path = (env.route.prefix || '') + options.path; options.vhost = env.route.vhost || options.vhost; } @@ -40,7 +39,7 @@ exports = module.exports = internals.Route = function (options, server, env) { Hoek.assert(options.path.match(internals.Route.pathRegex.validatePathEncoded) === null, 'Path cannot contain encoded non-reserved path characters:', options.path); Hoek.assert(options.path === '/' || options.path[options.path.length - 1] !== '/' || !server.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when server configured to strip:', options.path); - this.settings = Utils.cloneWithShallow(options.config, ['bind', 'plugins', 'app']) || {}; + this.settings = Hoek.cloneWithShallow(options.config, ['bind', 'plugins', 'app']) || {}; this.settings.handler = this.settings.handler || options.handler; Schema.assert('route', options, options.path); diff --git a/lib/server.js b/lib/server.js index 85494f714..4493c1c1d 100755 --- a/lib/server.js +++ b/lib/server.js @@ -16,7 +16,6 @@ var Router = require('./router'); var Schema = require('./schema'); var Views = require('./views'); var Ext = require('./ext'); -var Utils = require('./utils'); // Pack delayed required inline @@ -70,7 +69,7 @@ exports = module.exports = internals.Server = function (/* host, port, options * args[key] = argVal; } - this.settings = Utils.applyToDefaultsWithShallow(Defaults.server, args.options || {}, ['app', 'plugins', 'views']); + this.settings = Hoek.applyToDefaultsWithShallow(Defaults.server, args.options || {}, ['app', 'plugins', 'views']); Schema.assert('server', this.settings); this.settings.labels = Hoek.unique([].concat(this.settings.labels)); // Convert string to array and removes duplicates diff --git a/lib/utils.js b/lib/utils.js index 1f0e9eb7d..acb1a273e 100755 --- a/lib/utils.js +++ b/lib/utils.js @@ -26,58 +26,3 @@ exports.uniqueFilename = function (path) { var name = [Date.now(), process.pid, Crypto.randomBytes(8).toString('hex')].join('-'); return Path.join(path, name); }; - - -exports.cloneWithShallow = function (source, keys) { - - if (!source || - typeof source !== 'object') { - - return source; - } - - return internals.shallow(source, keys, function () { - - return Hoek.clone(source); - }); -}; - - -exports.applyToDefaultsWithShallow = function (defaults, options, keys) { - - return internals.shallow(options, keys, function () { - - return Hoek.applyToDefaults(defaults, options); - }); -}; - - -internals.shallow = function (source, keys, clone) { - - // Move shallow copy items to storage - - var storage = {}; - for (var i = 0, il = keys.length; i < il; ++i) { - var key = keys[i]; - if (source.hasOwnProperty(key)) { - storage[key] = source[key]; - source[key] = undefined; - } - } - - // Deep copy the rest - - var copy = clone(); - - // Shallow copy the stored items - - for (i = 0; i < il; ++i) { - var key = keys[i]; - if (storage.hasOwnProperty(key)) { - source[key] = storage[key]; - copy[key] = storage[key]; - } - } - - return copy; -}; \ No newline at end of file diff --git a/lib/views.js b/lib/views.js index 831275370..5272ee755 100755 --- a/lib/views.js +++ b/lib/views.js @@ -7,7 +7,6 @@ var Hoek = require('hoek'); var Defaults = require('./defaults'); var Schema = require('./schema'); var Response = require('./response'); -var Utils = require('./utils'); // Additional helper modules required in constructor @@ -18,7 +17,7 @@ var internals = {}; // View Manager -exports.Manager = internals.Manager = function (options, override) { +exports.Manager = internals.Manager = function (options, _override) { var self = this; @@ -29,9 +28,9 @@ exports.Manager = internals.Manager = function (options, override) { // Clone options - var defaults = Utils.applyToDefaultsWithShallow(Defaults.views, options, ['engines']); - if (override) { - Hoek.merge(defaults, override); + var defaults = Hoek.applyToDefaultsWithShallow(Defaults.views, options, ['engines']); + if (_override) { + Hoek.merge(defaults, _override); // _override cannot contain non-clonable objects } delete defaults.engines; @@ -62,7 +61,7 @@ exports.Manager = internals.Manager = function (options, override) { Schema.assert('view', config); engine.module = config.module; - engine.config = Utils.applyToDefaultsWithShallow(defaults, config, ['module']); + engine.config = Hoek.applyToDefaultsWithShallow(defaults, config, ['module']); } engine.suffix = '.' + extension; diff --git a/package.json b/package.json index 9401881f0..adda88290 100755 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "node": ">=0.10.22" }, "dependencies": { - "hoek": "2.x.x", + "hoek": "^2.3.x", "boom": "^2.4.x", "joi": "^4.4.x", "catbox": "^2.1.x", From ca5cd300d09246125e76ad616d3a84b352479de8 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sat, 31 May 2014 23:58:06 -0700 Subject: [PATCH 17/33] Move divider to kilt --- lib/divider.js | 107 ------------------------------------------------- lib/pack.js | 4 +- package.json | 1 + 3 files changed, 3 insertions(+), 109 deletions(-) delete mode 100755 lib/divider.js diff --git a/lib/divider.js b/lib/divider.js deleted file mode 100755 index 24afe2d78..000000000 --- a/lib/divider.js +++ /dev/null @@ -1,107 +0,0 @@ -// Load modules - -var Events = require('events'); -var Hoek = require('hoek'); - - -// Declare internals - -var internals = {}; - - -exports = module.exports = internals.Divider = function () { - - this._sources = []; - this._handlers = {}; - - Events.EventEmitter.call(this); -}; - -Hoek.inherits(internals.Divider, Events.EventEmitter); - - -internals.Divider.prototype.addEmitter = function (emitter) { - - var self = this; - - this._sources.push(emitter); - var types = Object.keys(this._events); - for (var i = 0, il = types.length; i < il; ++i) { - var type = types[i]; - emitter.on(type, this._handler(type)); - } -}; - - -internals.Divider.prototype._handler = function (type) { - - var self = this; - - if (this._handlers[type]) { - return this._handlers[type]; - } - - var handler = function (arg1, arg2, arg3, arg4, arg5) { - - self.emit(type, arg1, arg2, arg3, arg4, arg5); - self._unsubscribe(type); - }; - - this._handlers[type] = handler; - return handler; -}; - - -internals.Divider.prototype.on = internals.Divider.prototype.addListener = function (type, listener) { - - this._subscribe(type); - return Events.EventEmitter.prototype.on.apply(this, arguments); -}; - - -internals.Divider.prototype.once = function (type, listener) { - - this._subscribe(type); - return Events.EventEmitter.prototype.once.apply(this, arguments); -}; - - -internals.Divider.prototype._subscribe = function (type) { - - if (!this._events[type]) { - for (var i = 0, il = this._sources.length; i < il; ++i) { - this._sources[i].on(type, this._handler(type)); - } - } -}; - - -internals.Divider.prototype.removeListener = function (type, listener) { - - Events.EventEmitter.prototype.removeListener.apply(this, arguments); - this._unsubscribe(type); - return this; -}; - - -internals.Divider.prototype.removeAllListeners = function (type) { - - Events.EventEmitter.prototype.removeAllListeners.apply(this, arguments); - this._unsubscribe(type); - return this; -}; - - -internals.Divider.prototype._unsubscribe = function (type) { - - if (!this._events[type]) { - for (var i = 0, il = this._sources.length; i < il; ++i) { - this._sources[i].removeListener(type, this._handler(type)); - } - } - else if (type === undefined) { - for (var i = 0, il = this._sources.length; i < il; ++i) { - this._sources[i].removeAllListeners(); - } - } -}; \ No newline at end of file diff --git a/lib/pack.js b/lib/pack.js index 69406f257..faf796c9b 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -4,7 +4,7 @@ var Path = require('path'); var Async = require('async'); var Catbox = require('catbox'); var Hoek = require('hoek'); -var Divider = require('./divider'); +var Kilt = require('kilt'); var Server = require('./server'); var Views = require('./views'); var Defaults = require('./defaults'); @@ -37,7 +37,7 @@ exports = module.exports = internals.Pack = function (options) { this.list = {}; // Loaded plugins by name this.plugins = {}; // Exposed plugin properties by name - this.events = new Divider(); // Consolidated subscription to all servers + this.events = new Kilt(); // Consolidated subscription to all servers this.app = options.app || {}; if (options.cache) { diff --git a/package.json b/package.json index adda88290..5c6ca139d 100755 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "cryptiles": "2.x.x", "iron": "2.x.x", "topo": "1.x.x", + "kilt": "1.x.x", "async": "0.8.x", "multiparty": "3.2.x", "mime": "1.2.x", From 4d58ac7268c00d43b528d261b54f31ef0cf4d027 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 1 Jun 2014 00:24:29 -0700 Subject: [PATCH 18/33] Make plugin.events to selectable. Closes #1673 --- docs/Reference.md | 34 +++++++++++++++++----------------- lib/pack.js | 2 +- test/pack.js | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index ae698edde..4f7422c1c 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -73,7 +73,6 @@ - [`plugin.hapi`](#pluginhapi) - [`plugin.version`](#pluginversion) - [`plugin.app`](#pluginapp) - - [`plugin.events`](#pluginevents) - [`plugin.plugins`](#pluginplugins) - [`plugin.path(path)`](#pluginpathpath) - [`plugin.log(tags, [data, [timestamp]])`](#pluginlogtags-data-timestamp) @@ -91,6 +90,7 @@ - [`plugin.select(labels)`](#pluginselectlabels) - [`plugin.length`](#pluginlength) - [`plugin.servers`](#pluginservers) + - [`plugin.events`](#pluginevents) - [`plugin.expose(key, value)`](#pluginexposekey-value) - [`plugin.expose(obj)`](#pluginexposeobj) - [`plugin.route(options)`](#pluginrouteoptions) @@ -2386,22 +2386,6 @@ exports.register = function (plugin, options, next) { }; ``` -#### `plugin.events` - -The `pack.events' emitter. - -```javascript -exports.register = function (plugin, options, next) { - - plugin.events.on('internalError', function (request, err) { - - console.log(err); - }); - - next(); -}; -``` - #### `plugin.plugins` An object where each key is a plugin name and the value are the exposed properties by that plugin using [`plugin.expose()`](#pluginexposekey-value) @@ -2703,6 +2687,22 @@ exports.register = function (plugin, options, next) { }; ``` +#### `plugin.events` + +An emitter containing the events of all the selected servers. + +```javascript +exports.register = function (plugin, options, next) { + + plugin.events.on('internalError', function (request, err) { + + console.log(err); + }); + + next(); +}; +``` + #### `plugin.expose(key, value)` Exposes a property via `plugin.plugins[name]` (if added to the plugin root without first calling `plugin.select()`) and `server.plugins[name]` diff --git a/lib/pack.js b/lib/pack.js index faf796c9b..4980c2f26 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -263,6 +263,7 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen var methods = { length: selection.servers.length, servers: selection.servers, + events: new Kilt(selection.servers), select: function (/* labels */) { var labels = Hoek.flatten(Array.prototype.slice.call(arguments)); @@ -309,7 +310,6 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen root.version = root.hapi.version; root.app = self.app; root.plugins = self.plugins; - root.events = self.events; root.methods = self._methods.methods; root.expose = function (/* key, value */) { diff --git a/test/pack.js b/test/pack.js index b7230fc7b..8e574006b 100755 --- a/test/pack.js +++ b/test/pack.js @@ -1255,6 +1255,42 @@ describe('Pack', function () { }); }); + it('listens to events on selected servers', function (done) { + + var pack = new Hapi.Pack(); + pack.server({ labels: ['a'] }); + pack.server({ labels: ['b'] }); + pack.server({ labels: ['c'] }); + + var server1 = pack._servers[0]; + var server2 = pack._servers[1]; + var server3 = pack._servers[2]; + + var counter = 0; + var plugin = { + name: 'test', + register: function (plugin, options, next) { + + plugin.select(['a', 'b']).events.on('test', function () { + + ++counter; + }); + + next(); + } + }; + + pack.register(plugin, function (err) { + + expect(err).to.not.exist; + server1.emit('test'); + server2.emit('test'); + server3.emit('test'); + expect(counter).to.equal(2); + done(); + }); + }); + describe('#_provisionCache ', function () { it('throws when missing options', function (done) { From b5a031092f18c5beab3bd9de0751abb840e51cd5 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 1 Jun 2014 00:38:41 -0700 Subject: [PATCH 19/33] apply preselected plugins to expose. For #1663 --- lib/pack.js | 17 +++++++---------- test/pack.js | 6 ++++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/pack.js b/lib/pack.js index 4980c2f26..c03f59feb 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -269,9 +269,14 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen var labels = Hoek.flatten(Array.prototype.slice.call(arguments)); return step(labels, selection.index); }, - _expose: function (/* key, value */) { + expose: function (/* key, value */) { + + internals.expose(selection.servers, plugin, arguments); // server.plugins + + if (selection.servers.length === self._servers.length) { + internals.expose([self], plugin, arguments); // pack.plugins + } - internals.expose(selection.servers, plugin, arguments); }, route: function (options) { @@ -297,8 +302,6 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen } }; - methods.expose = methods._expose; - return methods; }; @@ -312,12 +315,6 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen root.plugins = self.plugins; root.methods = self._methods.methods; - root.expose = function (/* key, value */) { - - root._expose.apply(null, arguments); - internals.expose([self], plugin, arguments); - }; - root.log = function (tags, data, timestamp) { self.log(tags, data, timestamp); diff --git a/test/pack.js b/test/pack.js index 8e574006b..e9c19c0c1 100755 --- a/test/pack.js +++ b/test/pack.js @@ -1232,6 +1232,7 @@ describe('Pack', function () { register: function (plugin, options, next) { plugin.route({ method: 'GET', path: '/', handler: function (request, reply) { reply('ok'); } }); + plugin.expose('super', 'trooper'); next(); } }; @@ -1239,6 +1240,11 @@ describe('Pack', function () { pack.register(plugin, { labels: ['a', 'c'] }, function (err) { expect(err).to.not.exist; + expect(pack.plugins.test).to.not.exist; + expect(server1.plugins.test.super).to.equal('trooper'); + expect(server2.plugins.test).to.not.exist; + expect(server3.plugins.test.super).to.equal('trooper'); + server1.inject('/', function (res) { expect(res.statusCode).to.equal(200); From 8fc5db8ab110997925462446d0bf9586a37cab4d Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 1 Jun 2014 01:02:29 -0700 Subject: [PATCH 20/33] Remove pack.list. Closes #1675 --- docs/Reference.md | 5 ----- lib/pack.js | 13 +++++-------- test/pack.js | 2 +- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index 4f7422c1c..d14052e6a 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -2090,11 +2090,6 @@ Each `Pack` object instance has the following properties: Initialized via the pack `app` configuration option. Defaults to `{}`. - `events` - an `Events.EventEmitter` providing a consolidate emitter of all the events emitted from all member pack servers as well as the `'start'` and `'stop'` pack events. -- `list` - an object listing all the registered plugins where each key is a plugin name and the value is an object with: - - `name` - plugin name. - - `version` - plugin version. - - `path` - the plugin root path (where 'package.json' is located). - - `register()` - the [`exports.register()`](#exportsregisterplugin-options-next) function. - `plugins` - an object where each key is a plugin name and the value are the exposed properties by that plugin using [`plugin.expose()`](#pluginexposekey-value). diff --git a/lib/pack.js b/lib/pack.js index c03f59feb..14d2a267d 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -30,12 +30,11 @@ exports = module.exports = internals.Pack = function (options) { this._servers = []; // List of all pack server members this._byLabel = {}; // Server [ids] organized by labels this._byId = {}; // Servers indexed by id - this._env = {}; // Plugin-specific environment (e.g. views manager) this._caches = {}; // Cache clients this._methods = new Methods(this); // Server methods this._handlers = {}; // Registered handlers + this._list = {}; // Loaded plugins by name - this.list = {}; // Loaded plugins by name this.plugins = {}; // Exposed plugin properties by name this.events = new Kilt(); // Consolidated subscription to all servers this.app = options.app || {}; @@ -220,7 +219,7 @@ internals.Pack.prototype.register = function (plugins /*, [options], callback */ dependencies[deps].forEach(function (dep) { - Hoek.assert(self._env[dep], 'Plugin', deps, 'missing dependencies:', dep); + Hoek.assert(self._list[dep], 'Plugin', deps, 'missing dependencies:', dep); }); }); @@ -233,7 +232,7 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen var self = this; - Hoek.assert(!this._env[plugin.name], 'Plugin already registered:', plugin.name); + Hoek.assert(!this._list[plugin.name], 'Plugin already registered:', plugin.name); Schema.assert('register', registerOptions); // Setup environment @@ -248,11 +247,9 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen } }; - this._env[plugin.name] = env; - // Add plugin to servers lists - this.list[plugin.name] = plugin; + this._list[plugin.name] = plugin; // Setup pack interface @@ -345,7 +342,7 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen deps = [].concat(deps); deps.forEach(function (dep) { - if (!self._env[dep]) { + if (!self._list[dep]) { dependencies[plugin.name].push(dep); } }); diff --git a/test/pack.js b/test/pack.js index e9c19c0c1..6d571726c 100755 --- a/test/pack.js +++ b/test/pack.js @@ -256,7 +256,7 @@ describe('Pack', function () { server.pack.register(plugin, function (err) { expect(err).to.not.exist; - expect(server.pack.list['--steve'].version).to.equal('0.0.0'); + expect(server.pack._list['--steve'].version).to.equal('0.0.0'); server.inject('/', function (res) { expect(res.result).to.equal(Hapi.version); From 384bf6c3c6cd35948f99247347fe2fd16652ce74 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 1 Jun 2014 16:09:53 -0700 Subject: [PATCH 21/33] Move plugin register and dependency to selectable. Closes #1674 --- docs/Reference.md | 73 +++++++------- lib/pack.js | 239 ++++++++++++++++++++++++---------------------- lib/server.js | 1 + test/pack.js | 115 +++++++++++++++++----- 4 files changed, 257 insertions(+), 171 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index d14052e6a..8341dd408 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -76,14 +76,12 @@ - [`plugin.plugins`](#pluginplugins) - [`plugin.path(path)`](#pluginpathpath) - [`plugin.log(tags, [data, [timestamp]])`](#pluginlogtags-data-timestamp) - - [`plugin.dependency(deps, [after])`](#plugindependencydeps-after) - [`plugin.after(method)`](#pluginaftermethod) - [`plugin.views(options)`](#pluginviewsoptions) - [`plugin.method(name, fn, [options])`](#pluginmethodname-fn-options) - [`plugin.method(method)`](#pluginmethodmethod) - [`plugin.methods`](#pluginmethods) - [`plugin.cache(options)`](#plugincacheoptions) - - [`plugin.register(plugins, options, callback)`](#pluginregisterplugins-options-callback) - [`plugin.bind(bind)`](#pluginbind-bind) - [`plugin.handler(name, method)`](#pluginhandlername-method) - [Selectable methods and properties](#selectable-methods-and-properties) @@ -99,6 +97,8 @@ - [`plugin.auth.scheme(name, scheme)`](#pluginauthschemename-scheme) - [`plugin.auth.strategy(name, scheme, [mode], [options])`](#pluginauthstrategyname-scheme-mode-options) - [`plugin.ext(event, method, [options])`](#pluginextevent-method-options) + - [`plugin.register(plugins, options, callback)`](#pluginregisterplugins-options-callback) + - [`plugin.dependency(deps, [after])`](#plugindependencydeps-after) - [`Hapi.state`](#hapistate) - [`prepareValue(name, value, options, callback)`](#preparevaluename-value-options-callback) - [`Hapi.version`](#hapiversion) @@ -2421,36 +2421,6 @@ exports.register = function (plugin, options, next) { }; ``` -#### `plugin.dependency(deps, [after])` - -Declares a required dependency upon other plugins where: - -- `deps` - a single string or array of strings of plugin names which must be registered in order for this plugin to operate. Plugins listed - must be registered in the same pack transaction to allow validation of the dependency requirements. Does not provide version dependency which - should be implemented using [npm peer dependencies](http://blog.nodejs.org/2013/02/07/peer-dependencies/). -- `after` - an optional function called after all the specified dependencies have been registered and before the servers start. The function is only - called if the pack servers are started. If a circular dependency is created, the call will assert (e.g. two plugins each has an `after` function - to be called after the other). The function signature is `function(plugin, next)` where: - - `plugin` - the [plugin interface](#plugin-interface) object. - - `next` - the callback function the method must call to return control over to the application and complete the registration process. The function - signature is `function(err)` where: - - `err` - internal plugin error condition, which is returned back via the [`pack.start(callback)`](#packstartcallback) callback. A plugin - registration error is considered an unrecoverable event which should terminate the application. - -```javascript -exports.register = function (plugin, options, next) { - - plugin.dependency('yar', after); - next(); -}; - -var after = function (plugin, next) { - - // Additional plugin registration logic - next(); -}; -``` - #### `plugin.after(method)` Add a method to be called after all the required plugins have been registered and before the servers start. The function is only @@ -2796,6 +2766,45 @@ exports.register = function (plugin, options, next) { }; ``` +#### `plugin.require(plugins, [options], callback)` + +Registers a plugin with the current selection following the syntax of `pack.register()`. + +exports.register = function (plugin, options, next) { + + plugin.register({ plugin: require('furball'), options: { version: '/v' } }, next); +}; + +#### `plugin.dependency(deps, [after])` + +Declares a required dependency upon other plugins where: + +- `deps` - a single string or array of strings of plugin names which must be registered in order for this plugin to operate. Plugins listed + must be registered in the same pack transaction to allow validation of the dependency requirements. Does not provide version dependency which + should be implemented using [npm peer dependencies](http://blog.nodejs.org/2013/02/07/peer-dependencies/). +- `after` - an optional function called after all the specified dependencies have been registered and before the servers start. The function is only + called if the pack servers are started. If a circular dependency is created, the call will assert (e.g. two plugins each has an `after` function + to be called after the other). The function signature is `function(plugin, next)` where: + - `plugin` - the [plugin interface](#plugin-interface) object. + - `next` - the callback function the method must call to return control over to the application and complete the registration process. The function + signature is `function(err)` where: + - `err` - internal plugin error condition, which is returned back via the [`pack.start(callback)`](#packstartcallback) callback. A plugin + registration error is considered an unrecoverable event which should terminate the application. + +```javascript +exports.register = function (plugin, options, next) { + + plugin.dependency('yar', after); + next(); +}; + +var after = function (plugin, next) { + + // Additional plugin registration logic + next(); +}; +``` + ## `Hapi.state` #### `prepareValue(name, value, options, callback)` diff --git a/lib/pack.js b/lib/pack.js index 14d2a267d..e6faecad1 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -33,7 +33,6 @@ exports = module.exports = internals.Pack = function (options) { this._caches = {}; // Cache clients this._methods = new Methods(this); // Server methods this._handlers = {}; // Registered handlers - this._list = {}; // Loaded plugins by name this.plugins = {}; // Exposed plugin properties by name this.events = new Kilt(); // Consolidated subscription to all servers @@ -126,60 +125,100 @@ internals.Pack.prototype._server = function (server) { }; -internals.Pack.prototype.log = function (tags, data, timestamp, _server) { +internals.Pack.prototype._select = function (labels, subset) { - tags = (Array.isArray(tags) ? tags : [tags]); - var now = (timestamp ? (timestamp instanceof Date ? timestamp.getTime() : timestamp) : Date.now()); + var self = this; - var event = { - server: (_server ? _server.info.uri : undefined), - timestamp: now, - tags: tags, - data: data - }; + Hoek.assert(!labels || typeof labels === 'string' || Array.isArray(labels), 'Bad labels object type (undefined or array required)'); + labels = labels && [].concat(labels); - var tagsMap = Hoek.mapToObject(event.tags); + var ids = []; + if (labels) { + labels.forEach(function (label) { - this.events.emit('log', event, tagsMap); + ids = ids.concat(self._byLabel[label] || []); + }); - if (_server) { - _server.emit('log', event, tagsMap); + ids = Hoek.unique(ids); + } + else { + ids = Object.keys(this._byId); } - if (this._settings.debug && - this._settings.debug.request && - Hoek.intersect(tagsMap, this._settings.debug.request, true)) { + var result = { + servers: [], + index: {} + }; - console.error('Debug:', event.tags.join(', '), (data ? '\n ' + (data.stack || (typeof data === 'object' ? Utils.stringify(data) : data)) : '')); - } + ids.forEach(function (id) { + + if (!subset || + subset[id]) { + + result.servers.push(self._byId[id]); + result.index[id] = true; + } + }); + + return result; }; internals.Pack.prototype.register = function (plugins /*, [options], callback */) { - var self = this; + var options = (typeof arguments[1] === 'object' ? arguments[1] : {}); + var callback = (typeof arguments[1] === 'object' ? arguments[2] : arguments[1]); - plugins = [].concat(plugins); - var options = (arguments.length === 3 ? arguments[1] : {}); - var callback = (arguments.length === 3 ? arguments[2] : arguments[1]); + var state = { + dependencies: [] + }; + + return this._register(plugins, options, state, function (err) { + + if (err) { + return callback(err); + } + + for (var i = 0, il = state.dependencies.length; i < il; ++i) { + var dependency = state.dependencies[i]; + for (var s = 0, sl = dependency.servers.length; s < sl; ++s) { + var server = dependency.servers[s]; + for (var d = 0, dl = dependency.deps.length; d < dl; ++d) { + var dep = dependency.deps[d]; + Hoek.assert(server._registrations[dep], 'Plugin', dependency.plugin, 'missing dependency', dep, 'in server:', server.info.uri); + } + } + } + + return callback(); + }); +}; + + +internals.Pack.prototype._register = function (plugins, options, state, callback) { + + var self = this; /* var register = function (plugin, options, next) { next(); } register.attributes = { name: 'plugin', version: '1.1.1', - pkg: require('../package.json') + pkg: require('../package.json'), + multiple: false }; plugin = { register: register, // plugin: { register } when assigned a directly required module name: 'plugin', // || register.attributes.name || register.attributes.pkg.name version: '1.1.1', // -optional- || register.attributes.version || register.attributes.pkg.version + multiple: false, // -optional- || register.attributes.multiple options: {} // -optional- }; */ var registrations = []; + plugins = [].concat(plugins); for (var i = 0, il = plugins.length; i < il; ++i) { var plugin = plugins[i]; var hint = (plugins.length > 1 ? '(' + i + ')' : ''); @@ -198,41 +237,26 @@ internals.Pack.prototype.register = function (plugins /*, [options], callback */ register: register, name: plugin.name || attributes.name || attributes.pkg.name, version: plugin.version || attributes.version || (attributes.pkg && attributes.pkg.version) || '0.0.0', + multiple: plugin.multiple || attributes.multiple || false, options: plugin.options }; registrations.push(item); } - var dependencies = {}; Async.forEachSeries(registrations, function (item, next) { - self._register(item, options, dependencies, next); - }, - function (err) { - - if (err) { - return callback(err); - } - - Object.keys(dependencies).forEach(function (deps) { - - dependencies[deps].forEach(function (dep) { - - Hoek.assert(self._list[dep], 'Plugin', deps, 'missing dependencies:', dep); - }); - }); - - return callback(); - }); + self._plugin(item, options, state, next); + }, callback); }; -internals.Pack.prototype._register = function (plugin, registerOptions, dependencies, callback) { +internals.Pack.prototype._plugin = function (plugin, registerOptions, state, callback) { var self = this; - Hoek.assert(!this._list[plugin.name], 'Plugin already registered:', plugin.name); + // Validate options + Schema.assert('register', registerOptions); // Setup environment @@ -247,9 +271,10 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen } }; - // Add plugin to servers lists - - this._list[plugin.name] = plugin; + if (state.route) { + env.route.prefix = (state.route.prefix || '') + (env.route.prefix || '') || undefined; + env.route.vhost = state.route.vhost || env.route.vhost; + } // Setup pack interface @@ -273,7 +298,6 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen if (selection.servers.length === self._servers.length) { internals.expose([self], plugin, arguments); // pack.plugins } - }, route: function (options) { @@ -296,6 +320,30 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen ext: function () { self._applySync(selection.servers, Server.prototype._ext, [arguments[0], arguments[1], arguments[2], env]); + }, + dependency: function (deps, after) { + + Hoek.assert(!after || typeof after === 'function', 'Invalid after method'); + + deps = [].concat(deps); + state.dependencies.push({ plugin: plugin.name, servers: selection.servers, deps: deps }); + + if (after) { + self._ext.add('onPreStart', after, { after: deps }, env); + } + }, + register: function (plugins /*, [options], callback */) { + + var options = (typeof arguments[1] === 'object' ? arguments[1] : {}); + var callback = (typeof arguments[1] === 'object' ? arguments[2] : arguments[1]); + + var localState = { + dependecies: state.dependecies, + route: env.route, + selection: selection.index + }; + + internals.Pack.prototype._register.call(self, plugins, options, localState, callback); } }; @@ -304,7 +352,7 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen // Setup root pack object - var root = step(registerOptions.labels); + var root = step(registerOptions.labels, state.selection); root.hapi = require('../'); root.version = root.hapi.version; @@ -317,41 +365,6 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen self.log(tags, data, timestamp); }; - root.register = function (plugins /*, [options], callback */) { - - var options = (arguments.length === 3 ? arguments[1] : {}); - var callback = (arguments.length === 3 ? arguments[2] : arguments[1]); - - if (env.route.prefix || - env.route.vhost) { - - options = Hoek.clone(options); - options.route = options.route || {}; - options.route.prefix = (env.route.prefix || '') + (options.route.prefix || '') || undefined; - options.route.vhost = env.route.vhost || options.route.vhost; - } - - internals.Pack.prototype.register.call(self, plugins, options, callback); - }; - - root.dependency = function (deps, after) { - - Hoek.assert(!after || typeof after === 'function', 'Invalid after method'); - - dependencies[plugin.name] = dependencies[plugin.name] || []; - deps = [].concat(deps); - deps.forEach(function (dep) { - - if (!self._list[dep]) { - dependencies[plugin.name].push(dep); - } - }); - - if (after) { - self._ext.add('onPreStart', after, { after: deps }, env); - } - }; - root.after = function (method) { self._ext.add('onPreStart', method, {}, env); @@ -400,6 +413,14 @@ internals.Pack.prototype._register = function (plugin, registerOptions, dependen env.root = root; + // Protect against multiple registrations + + for (var i = 0, il = root.servers.length; i < il; ++i) { + var server = root.servers[i]; + Hoek.assert(plugin.multiple || !server._registrations[plugin.name], 'Plugin', plugin.name, 'already registered in:', server.info.uri); + server._registrations[plugin.name] = plugin; + } + // Register plugin.register.call(null, root, plugin.options || {}, callback); @@ -424,44 +445,32 @@ internals.expose = function (dests, plugin, args) { }; -internals.Pack.prototype._select = function (labels, subset) { - - var self = this; - - Hoek.assert(!labels || typeof labels === 'string' || Array.isArray(labels), 'Bad labels object type (undefined or array required)'); - labels = labels && [].concat(labels); - - var ids = []; - if (labels) { - labels.forEach(function (label) { - - ids = ids.concat(self._byLabel[label] || []); - }); +internals.Pack.prototype.log = function (tags, data, timestamp, _server) { - ids = Hoek.unique(ids); - } - else { - ids = Object.keys(this._byId); - } + tags = (Array.isArray(tags) ? tags : [tags]); + var now = (timestamp ? (timestamp instanceof Date ? timestamp.getTime() : timestamp) : Date.now()); - var result = { - servers: [], - index: {} + var event = { + server: (_server ? _server.info.uri : undefined), + timestamp: now, + tags: tags, + data: data }; - ids.forEach(function (id) { + var tagsMap = Hoek.mapToObject(event.tags); - if (subset && - !subset[id]) { + this.events.emit('log', event, tagsMap); - return; - } + if (_server) { + _server.emit('log', event, tagsMap); + } - result.servers.push(self._byId[id]); - result.index[id] = true; - }); + if (this._settings.debug && + this._settings.debug.request && + Hoek.intersect(tagsMap, this._settings.debug.request, true)) { - return result; + console.error('Debug:', event.tags.join(', '), (data ? '\n ' + (data.stack || (typeof data === 'object' ? Utils.stringify(data) : data)) : '')); + } }; diff --git a/lib/server.js b/lib/server.js index 4493c1c1d..def0632f5 100755 --- a/lib/server.js +++ b/lib/server.js @@ -121,6 +121,7 @@ exports = module.exports = internals.Server = function (/* host, port, options * this._ext = new Ext(['onRequest', 'onPreAuth', 'onPostAuth', 'onPreHandler', 'onPostHandler', 'onPreResponse']); this._stateDefinitions = {}; + this._registrations = {}; if (args.pack) { this.pack = args.pack; diff --git a/test/pack.js b/test/pack.js index 6d571726c..34b0fe5b2 100755 --- a/test/pack.js +++ b/test/pack.js @@ -98,7 +98,7 @@ describe('Pack', function () { }); }); - it('registers plugins with options', function (done) { + it('registers plugin with options', function (done) { var pack = new Hapi.Pack(); pack.server({ labels: ['a', 'b'] }); @@ -120,7 +120,7 @@ describe('Pack', function () { }); }); - it('registers plugin via server plugin interface', function (done) { + it('registers plugin via server pack interface', function (done) { var plugin = { name: 'test', @@ -256,7 +256,7 @@ describe('Pack', function () { server.pack.register(plugin, function (err) { expect(err).to.not.exist; - expect(server.pack._list['--steve'].version).to.equal('0.0.0'); + expect(server._registrations['--steve'].version).to.equal('0.0.0'); server.inject('/', function (res) { expect(res.result).to.equal(Hapi.version); @@ -284,7 +284,7 @@ describe('Pack', function () { done(); }); - it('registers required plugin', function (done) { + it('registers plugin with exposed api', function (done) { var pack = new Hapi.Pack(); pack.server({ labels: ['s1', 'a', 'b'] }); @@ -308,41 +308,78 @@ describe('Pack', function () { }); }); - it('registers a plugin with options', function (done) { + it('prevents plugin from multiple registrations', function (done) { - var pack = new Hapi.Pack(); - pack.server({ labels: ['a', 'b'] }); + var plugin = { + name: 'test', + register: function (plugin, options, next) { + + plugin.route({ method: 'GET', path: '/a', handler: function (request, reply) { reply('a'); } }); + next(); + } + }; - pack.register({ plugin: require('./pack/--test1'), options: { something: true } }, function (err) { + var server = new Hapi.Server(); + server.pack.register(plugin, function (err) { expect(err).to.not.exist; + expect(function () { + + server.pack.register(plugin, function (err) { }); + }).to.throw('Plugin test already registered in: ' + server.info.uri); + done(); }); }); - it('requires plugin via server plugin interface', function (done) { + it('allows plugin multiple registrations (attributes)', function (done) { var plugin = { name: 'test', register: function (plugin, options, next) { - plugin.route({ method: 'GET', path: '/a', handler: function (request, reply) { reply('a'); } }); + plugin.app.x = plugin.app.x ? plugin.app.x + 1 : 1; next(); } }; + plugin.register.attributes = { multiple: true }; + var server = new Hapi.Server(); server.pack.register(plugin, function (err) { expect(err).to.not.exist; - expect(routesList(server)).to.deep.equal(['/a']); + server.pack.register(plugin, function (err) { - expect(function () { + expect(err).to.not.exist; + expect(server.pack.app.x).to.equal(2); + done(); + }); + }); + }); - server.pack.register(plugin, function (err) { }); - }).to.throw(); + it('allows plugin multiple registrations (property)', function (done) { - done(); + var plugin = { + name: 'test', + multiple: true, + register: function (plugin, options, next) { + + plugin.app.x = plugin.app.x ? plugin.app.x + 1 : 1; + next(); + } + }; + + var server = new Hapi.Server(); + server.pack.register(plugin, function (err) { + + expect(err).to.not.exist; + server.pack.register(plugin, function (err) { + + expect(err).to.not.exist; + expect(server.pack.app.x).to.equal(2); + done(); + }); }); }); @@ -548,16 +585,46 @@ describe('Pack', function () { }); }); - it('fails to require missing module', function (done) { + it('registers a plugin on selection inside a plugin', function (done) { var pack = new Hapi.Pack(); - pack.server({ labels: ['a', 'b'] }); + pack.server({ labels: ['a'] }); + pack.server({ labels: ['b'] }); + pack.server({ labels: ['c'] }); - expect(function () { + var server1 = pack._servers[0]; + var server2 = pack._servers[1]; + var server3 = pack._servers[2]; - pack.register(require('./pack/none'), function (err) { }); - }).to.throw('Cannot find module'); - done(); + var child = { + name: 'child', + register: function (plugin, options, next) { + + plugin.expose('key2', 2); + next(); + } + }; + + var plugin = { + name: 'test', + register: function (plugin, options, next) { + + plugin.expose('key1', 1); + plugin.select('a').register(child, next); + } + }; + + pack.register(plugin, { labels: ['a', 'b'] }, function (err) { + + expect(err).to.not.exist; + expect(server1.plugins.test.key1).to.equal(1); + expect(server1.plugins.child.key2).to.equal(2); + expect(server2.plugins.test.key1).to.equal(1); + expect(server2.plugins.child).to.not.exist; + expect(server3.plugins.test).to.not.exist; + expect(server3.plugins.child).to.not.exist; + done(); + }); }); it('starts and stops', function (done) { @@ -687,7 +754,7 @@ describe('Pack', function () { expect(function () { server.pack.register(require('./pack/--deps1'), function (err) { }); - }).to.throw('Plugin --deps1 missing dependencies: --deps2'); + }).to.throw('Plugin --deps1 missing dependency --deps2 in server: http://monterey:80'); done(); }); @@ -706,7 +773,7 @@ describe('Pack', function () { expect(function () { server.pack.register(plugin, function (err) { }); - }).to.throw('Plugin test missing dependencies: none'); + }).to.throw('Plugin test missing dependency none in server: http://monterey:80'); done(); }); @@ -717,7 +784,7 @@ describe('Pack', function () { var domain = Domain.create(); domain.on('error', function (err) { - expect(err.message).to.equal('Plugin --deps1 missing dependencies: --deps2'); + expect(err.message).to.equal('Plugin --deps1 missing dependency --deps2 in server: http://monterey:80'); done(); }); From 62bd628f1d938fe27f065a64572d2b5ced5166fe Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 1 Jun 2014 19:29:58 -0700 Subject: [PATCH 22/33] manifest support for plugin select. Closes #1677 --- docs/Reference.md | 29 +++++++- lib/pack.js | 68 +++++++++++++++--- lib/schema.js | 2 +- test/pack.js | 119 +++++++++++++++++++++++++++++++- test/pack/--custom/index.js | 20 ++++++ test/pack/--custom/package.json | 7 ++ 6 files changed, 230 insertions(+), 15 deletions(-) create mode 100755 test/pack/--custom/index.js create mode 100755 test/pack/--custom/package.json diff --git a/docs/Reference.md b/docs/Reference.md index 8341dd408..dabe2e782 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -2145,7 +2145,7 @@ Registers a plugin where: - a module plugin object. - a manually constructed plugin object. - `options` - optional registration options (used by **hapi** and is not passed to the plugin): - - `labels` - string or array of strings of labels or pre-select for plugin registration. + - `select` - string or array of strings of labels to pre-select for plugin registration. - `route` - apply modifiers to any routes added by the plugin: - `prefix` - string added as prefix to any route path (must begin with `'/'`). If a plugin registers a child plugin the `prefix` is passed on to the child or is added in front of the child-specific prefix. @@ -2179,6 +2179,8 @@ server.pack.register({ Manually constructed plugin is an object containing: - `name` - plugin name. - `version` - an optional plugin version. Defaults to `'0.0.0'`. +- `multiple` - an optional boolean indicating if the plugin is safe to register multiple time with the same server. + Defaults to `false`. - `register` - the [`register()`](#exportsregisterplugin-options-next) function. - `options` - optional configuration object which is passed to the plugin via the `options` argument in [`exports.register()`](#exportsregisterplugin-options-next). @@ -2214,7 +2216,11 @@ and registering plugins where: - `host`, `port`, `options` - the same as described in [`new Server()`](#new-serverhost-port-options) with the exception that the `cache` option is not allowed and must be configured via the pack `cache` option. The `host` and `port` keys can be set to an environment variable by prefixing the variable name with `'$env.'`. - - `plugins` - an object where each key is a plugin name, and each value is the `options` object used to register that plugin. + - `plugins` - an object where each key is a plugin name, and each value is one of: + - the `options` object passed to the plugin on registration. + - an array of object where: + - `options` - the object passed to the plugin on registration. + - any key supported by the `pack.register()` options used for registration (e.g. `select`). - `options` - optional compose configuration: - `relativeTo` - path prefix used when loading plugins using node's `require()`. The `relativeTo` path prefix is added to any relative plugin name (i.e. beings with `'./'`). All other module names are required as-is and will be looked up from the location @@ -2253,7 +2259,15 @@ var manifest = { cookieOptions: { password: 'secret' } - } + }, + 'furball': [ + { + select: 'web', + options: { + version: '/v' + } + } + ] } }; @@ -2313,6 +2327,15 @@ exports.register.attributes = { }; ``` +The `multiple` attributes specifies that a plugin is safe to register multiple times with the same server. + +```javascript +exports.register.attributes = { + multiple: true, + pkg: require('./package.json') +}; +``` + #### `exports.register(plugin, options, next)` Registers the plugin where: diff --git a/lib/pack.js b/lib/pack.js index e6faecad1..aea49ff67 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -352,7 +352,7 @@ internals.Pack.prototype._plugin = function (plugin, registerOptions, state, cal // Setup root pack object - var root = step(registerOptions.labels, state.selection); + var root = step(registerOptions.select, state.selection); root.hapi = require('../'); root.version = root.hapi.version; @@ -624,7 +624,7 @@ internals.Pack.prototype._invoke = function (event, callback) { /* -var config = { +var config1 = { pack: { cache: 'redis', app: { @@ -650,7 +650,16 @@ var config = { furball: { version: false, plugins: '/' - } + }, + other: [ + { + select: ['b'], + options: { + version: false, + plugins: '/' + } + } + ] } }; */ @@ -691,10 +700,10 @@ internals.Pack.compose = function (manifest /*, [options], callback */) { // Load plugin - var plugins = []; var names = Object.keys(manifest.plugins); - names.forEach(function (name) { + Async.forEachSeries(names, function (name, nextName) { + var item = manifest.plugins[name]; var path = name; if (options.relativeTo && path[0] === '.') { @@ -702,10 +711,53 @@ internals.Pack.compose = function (manifest /*, [options], callback */) { path = Path.join(options.relativeTo, path); } - plugins.push({ plugin: require(path), options: manifest.plugins[name] }); - }); + /* + simple: { + key: 'value' + }, + custom: [ + { + select: ['b'], + options: { + key: 'value' + } + } + ] + */ + + var plugins = []; + if (Array.isArray(item)) { + item.forEach(function (instance) { + + var registerOptions = Hoek.cloneWithShallow(instance, 'options'); + delete registerOptions.options; + + plugins.push({ + module: { + plugin: require(path), + options: instance.options + }, + apply: registerOptions + }); + }); + } + else { + plugins.push({ + module: { + plugin: require(path), + options: item + }, + apply: {} + }); + } - pack.register(plugins, function (err) { + Async.forEachSeries(plugins, function (plugin, nextRegister) { + + + pack.register(plugin.module, plugin.apply, nextRegister); + }, nextName); + }, + function (err) { return callback(err, pack); }); diff --git a/lib/schema.js b/lib/schema.js index 28f5b90b4..88c01583f 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -339,5 +339,5 @@ internals.register = Joi.object({ prefix: Joi.string().regex(/^\/.+/), vhost: internals.vhost }), - labels: internals.labels + select: internals.labels }); \ No newline at end of file diff --git a/test/pack.js b/test/pack.js index 34b0fe5b2..9cc1505f6 100755 --- a/test/pack.js +++ b/test/pack.js @@ -614,7 +614,7 @@ describe('Pack', function () { } }; - pack.register(plugin, { labels: ['a', 'b'] }, function (err) { + pack.register(plugin, { select: ['a', 'b'] }, function (err) { expect(err).to.not.exist; expect(server1.plugins.test.key1).to.equal(1); @@ -1268,7 +1268,7 @@ describe('Pack', function () { } }; - pack.register(plugin, { labels: 'a' }, function (err) { + pack.register(plugin, { select: 'a' }, function (err) { expect(err).to.not.exist; server1.inject('/', function (res) { @@ -1304,7 +1304,7 @@ describe('Pack', function () { } }; - pack.register(plugin, { labels: ['a', 'c'] }, function (err) { + pack.register(plugin, { select: ['a', 'c'] }, function (err) { expect(err).to.not.exist; expect(pack.plugins.test).to.not.exist; @@ -1660,5 +1660,118 @@ describe('Pack', function () { }).to.throw('Manifest missing servers definition'); done(); }); + + it('composes pack with plugin registration options', function (done) { + + var manifest = { + pack: { + cache: { + engine: 'catbox-memory' + }, + app: { + my: 'special-value' + } + }, + servers: [ + { + port: 0, + options: { + labels: ['a', 'b'] + } + }, + { + port: 0, + options: { + labels: ['b', 'c'] + } + } + ], + plugins: { + '../test/pack/--custom': [ + { + options: { + path: '/' + } + }, + { + select: 'a', + options: { + path: '/a' + } + }, + { + select: 'b', + options: { + path: '/b' + } + }, + { + route: { + prefix: '/steve' + }, + options: { + path: '/a' + } + } + ] + } + }; + + Hapi.Pack.compose(manifest, function (err, pack) { + + expect(err).to.not.exist; + + var server1 = pack._servers[0]; + var server2 = pack._servers[1]; + + server1.inject('/', function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('/'); + + server2.inject('/', function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('/'); + + server1.inject('/a', function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('/a'); + + server2.inject('/a', function (res) { + + expect(res.statusCode).to.equal(404); + + server1.inject('/b', function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('/b'); + + server2.inject('/b', function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('/b'); + + server1.inject('/steve/a', function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('/a'); + + server2.inject('/steve/a', function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('/a'); + done(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); }); }); diff --git a/test/pack/--custom/index.js b/test/pack/--custom/index.js new file mode 100755 index 000000000..5b366540f --- /dev/null +++ b/test/pack/--custom/index.js @@ -0,0 +1,20 @@ +// Declare internals + +var internals = {}; + + +// Plugin registration + +exports.register = function (plugin, options, next) { + + plugin.route({ method: 'GET', path: options.path, handler: function (request, reply) { reply(options.path); } }); + return next(); +}; + + +exports.register.attributes = { + name: '--custom', + version: '1.0.0', + multiple: true +}; + diff --git a/test/pack/--custom/package.json b/test/pack/--custom/package.json new file mode 100755 index 000000000..e575efc1d --- /dev/null +++ b/test/pack/--custom/package.json @@ -0,0 +1,7 @@ +{ + "name": "--custom", + "description": "Test plugin module", + "version": "0.0.1", + "private": true, + "main": "./index" +} From 757cb4861a5e624c86b6faf63e80b0c7d924a360 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Thu, 29 May 2014 08:20:30 -0700 Subject: [PATCH 23/33] Fix test --- test/pack.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/pack.js b/test/pack.js index 9cc1505f6..2e9222e00 100755 --- a/test/pack.js +++ b/test/pack.js @@ -754,7 +754,7 @@ describe('Pack', function () { expect(function () { server.pack.register(require('./pack/--deps1'), function (err) { }); - }).to.throw('Plugin --deps1 missing dependency --deps2 in server: http://monterey:80'); + }).to.throw('Plugin --deps1 missing dependency --deps2 in server: ' + server.info.uri); done(); }); @@ -773,7 +773,7 @@ describe('Pack', function () { expect(function () { server.pack.register(plugin, function (err) { }); - }).to.throw('Plugin test missing dependency none in server: http://monterey:80'); + }).to.throw('Plugin test missing dependency none in server: ' + server.info.uri); done(); }); @@ -784,7 +784,7 @@ describe('Pack', function () { var domain = Domain.create(); domain.on('error', function (err) { - expect(err.message).to.equal('Plugin --deps1 missing dependency --deps2 in server: http://monterey:80'); + expect(err.message).to.equal('Plugin --deps1 missing dependency --deps2 in server: ' + server.info.uri); done(); }); From 85c3248e89519a7246eda6b50fbf42beeb3b5615 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 1 Jun 2014 21:16:18 -0700 Subject: [PATCH 24/33] Fix pack event emitter. For #1673 --- lib/pack.js | 12 +++++++----- package.json | 2 +- test/pack.js | 22 +++++++++++++++++----- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/lib/pack.js b/lib/pack.js index aea49ff67..41c87410e 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -1,6 +1,7 @@ // Load modules var Path = require('path'); +var Events = require('events'); var Async = require('async'); var Catbox = require('catbox'); var Hoek = require('hoek'); @@ -33,9 +34,10 @@ exports = module.exports = internals.Pack = function (options) { this._caches = {}; // Cache clients this._methods = new Methods(this); // Server methods this._handlers = {}; // Registered handlers + this._events = new Events.EventEmitter(); // Pack-only events this.plugins = {}; // Exposed plugin properties by name - this.events = new Kilt(); // Consolidated subscription to all servers + this.events = new Kilt(this._events); // Consolidated server events this.app = options.app || {}; if (options.cache) { @@ -285,7 +287,7 @@ internals.Pack.prototype._plugin = function (plugin, registerOptions, state, cal var methods = { length: selection.servers.length, servers: selection.servers, - events: new Kilt(selection.servers), + events: new Kilt(selection.servers, self._events), select: function (/* labels */) { var labels = Hoek.flatten(Array.prototype.slice.call(arguments)); @@ -459,7 +461,7 @@ internals.Pack.prototype.log = function (tags, data, timestamp, _server) { var tagsMap = Hoek.mapToObject(event.tags); - this.events.emit('log', event, tagsMap); + this._events.emit('log', event, tagsMap); if (_server) { _server.emit('log', event, tagsMap); @@ -496,7 +498,7 @@ internals.Pack.prototype.start = function (callback) { self._caches[cache].client.start(next); }, function (err) { - self.events.emit('start'); + self._events.emit('start'); if (callback) { return callback(err); @@ -518,7 +520,7 @@ internals.Pack.prototype.stop = function (options, callback) { this._apply(this._servers, Server.prototype._stop, [options], function () { - self.events.emit('stop'); + self._events.emit('stop'); if (callback) { callback(); diff --git a/package.json b/package.json index 5c6ca139d..4683b9c39 100755 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "cryptiles": "2.x.x", "iron": "2.x.x", "topo": "1.x.x", - "kilt": "1.x.x", + "kilt": "^1.1.x", "async": "0.8.x", "multiparty": "3.2.x", "mime": "1.2.x", diff --git a/test/pack.js b/test/pack.js index 2e9222e00..98c354993 100755 --- a/test/pack.js +++ b/test/pack.js @@ -1331,9 +1331,9 @@ describe('Pack', function () { it('listens to events on selected servers', function (done) { var pack = new Hapi.Pack(); - pack.server({ labels: ['a'] }); - pack.server({ labels: ['b'] }); - pack.server({ labels: ['c'] }); + pack.server(0, { labels: ['a'] }); + pack.server(0, { labels: ['b'] }); + pack.server(0, { labels: ['c'] }); var server1 = pack._servers[0]; var server2 = pack._servers[1]; @@ -1349,6 +1349,11 @@ describe('Pack', function () { ++counter; }); + plugin.select(['a']).events.on('start', function () { + + ++counter; + }); + next(); } }; @@ -1359,8 +1364,15 @@ describe('Pack', function () { server1.emit('test'); server2.emit('test'); server3.emit('test'); - expect(counter).to.equal(2); - done(); + + pack.start(function () { + + pack.stop(function () { + + expect(counter).to.equal(3); + done(); + }); + }); }); }); From b7c304dcc4ea6b7d77b5e8534023ff39130a49d1 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 2 Jun 2014 13:28:45 -0700 Subject: [PATCH 25/33] server.location(). Closes #1678 --- docs/Reference.md | 15 +++++++++++++++ lib/response/headers.js | 12 ++++++++---- lib/server.js | 7 +++++++ test/server.js | 31 +++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index dabe2e782..22ddf36c0 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -29,6 +29,7 @@ - [`server.method(method)`](#servermethodmethod) - [`server.inject(options, callback)`](#serverinjectoptions-callback) - [`server.handler(name, method)`](#serverhandlername-method) + - [`server.location(uri, [request])`](#serverlocationuri-request) - [`Server` events](#server-events) - [Request object](#request-object) - [`request` properties](#request-properties) @@ -1301,6 +1302,20 @@ server.route({ server.start(); ``` +#### `server.location(uri, [request])` + +Converts the provided URI to an absolute URI using the server or request configuration where: +- `uri` - the relative URI. +- `request` - an optional request object for using the request host header if no server location has been configured. + +```javascript +var Hapi = require('hapi'); +var server = Hapi.createServer('localhost', 8000); + +console.log(server.location('/relative')); +``` + + ### `Server` events The server object inherits from `Events.EventEmitter` and emits the following events: diff --git a/lib/response/headers.js b/lib/response/headers.js index 99f9b411a..059322be4 100755 --- a/lib/response/headers.js +++ b/lib/response/headers.js @@ -21,7 +21,7 @@ exports.apply = function (request, next) { } if (response.settings.location) { - response._header('location', internals.location(response.settings.location, request)); + response._header('location', exports.location(response.settings.location, request.server, request)); } internals.cors(response, request); @@ -39,11 +39,15 @@ exports.apply = function (request, next) { }; -internals.location = function (uri, request) { +exports.location = function (uri, server, request) { var isAbsolute = (uri.match(/^\w+\:\/\//)); - var baseUri = request.server.settings.location || (request.server.info.protocol + '://' + (request.info.host || (request.server.info.host + ':' + request.server.info.port))); - return (isAbsolute || !baseUri ? uri : baseUri + (uri.charAt(0) === '/' ? '' : '/') + uri); + if (isAbsolute) { + return uri; + } + + var baseUri = server.settings.location || (server.info.protocol + '://' + ((request && request.info.host) || (server.info.host + ':' + server.info.port))); + return baseUri + (uri.charAt(0) === '/' ? '' : '/') + uri; }; diff --git a/lib/server.js b/lib/server.js index def0632f5..880acf2e6 100755 --- a/lib/server.js +++ b/lib/server.js @@ -16,6 +16,7 @@ var Router = require('./router'); var Schema = require('./schema'); var Views = require('./views'); var Ext = require('./ext'); +var Headers = require('./response/headers'); // Pack delayed required inline @@ -514,3 +515,9 @@ internals.Server.prototype.handler = function () { return this.pack._handler.apply(this.pack, arguments); }; + + +internals.Server.prototype.location = function (uri, request) { + + return Headers.location(uri, this, request); +}; diff --git a/test/server.js b/test/server.js index 626ad33de..63ddd19a1 100755 --- a/test/server.js +++ b/test/server.js @@ -1565,4 +1565,35 @@ describe('Server', function () { done(); }) }); + + describe('#location', function () { + + it('returns back the same absolute location', function (done) { + + var server = new Hapi.Server({ location: 'http://example.net' }); + expect(server.location('http://example.com/test')).to.equal('http://example.com/test'); + done(); + }); + + it('returns back the an absolute location using server config', function (done) { + + var server = new Hapi.Server({ location: 'http://example.net' }); + expect(server.location('/test')).to.equal('http://example.net/test'); + done(); + }); + + it('returns back the an absolute location using request host', function (done) { + + var server = new Hapi.Server(); + expect(server.location('/test', { info: { host: 'example.edu' } })).to.equal('http://example.edu/test'); + done(); + }); + + it('returns back the an absolute location using server info', function (done) { + + var server = new Hapi.Server(); + expect(server.location('/test')).to.equal('http://0.0.0.0:80/test'); + done(); + }); + }); }); From 48511f8a41b1d2e63d6c827b7241efb1fbf8ce84 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 2 Jun 2014 17:52:39 -0700 Subject: [PATCH 26/33] Cookie specific overrides. Closes #1679 --- docs/Reference.md | 3 ++ lib/defaults.js | 9 ++++++ lib/schema.js | 20 +++++++++++++ lib/server.js | 4 ++- lib/state.js | 73 ++++++++++++++++++++++------------------------- test/state.js | 30 +++++++++++++++++++ 6 files changed, 99 insertions(+), 40 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index 22ddf36c0..d94d1ede5 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -925,6 +925,9 @@ can be registered with the server using the `server.state()` method, where: - `password` - password used for HMAC key generation. - `password` - password used for `'iron'` encoding. - `iron` - options for `'iron'` encoding. Defaults to [`require('iron').defaults`](https://github.com/hueniverse/iron#options). + - `failAction` - overrides the default server `state.cookies.failAction` setting. + - `clearInvalid` - overrides the default server `state.cookies.clearInvalid` setting. + - `strictHeader` - overrides the default server `state.cookies.strictHeader` setting. ```javascript // Set cookie definition diff --git a/lib/defaults.js b/lib/defaults.js index 01a710cb6..e92c43763 100755 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -140,6 +140,9 @@ exports.cors = { credentials: false }; + +// Security headers + exports.security = { hsts: 15768000, xframe: 'deny', @@ -160,6 +163,12 @@ exports.cache = { // Primary cache configurati exports.state = { + // Validation settings + + strictHeader: undefined, // Defaults to server.settings.state.cookies.strictHeader + failAction: undefined, // Defaults to server.settings.state.cookies.failAction + clearInvalid: undefined, // Defaults to server.settings.state.cookies.clearInvalid + // Cookie attributes isSecure: false, diff --git a/lib/schema.js b/lib/schema.js index 88c01583f..5dd185ab9 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -340,4 +340,24 @@ internals.register = Joi.object({ vhost: internals.vhost }), select: internals.labels +}); + + +internals.state = Joi.object({ + strictHeader: Joi.boolean(), + failAction: Joi.string().valid('error', 'log', 'ignore'), + clearInvalid: Joi.boolean(), + isSecure: Joi.boolean(), + isHttpOnly: Joi.boolean(), + path: Joi.string(), + domain: Joi.string(), + ttl: Joi.number(), + encoding: Joi.string().valid('base64json', 'base64', 'form', 'iron', 'none'), + sign: Joi.object({ + password: Joi.string(), + integrity: Joi.object() + }), + iron: Joi.object(), + password: Joi.string(), + autoValue: Joi.any() }); \ No newline at end of file diff --git a/lib/server.js b/lib/server.js index 880acf2e6..a6e7c2127 100755 --- a/lib/server.js +++ b/lib/server.js @@ -463,7 +463,9 @@ internals.Server.prototype.state = function (name, options) { Hoek.assert(name && typeof name === 'string', 'Invalid name'); Hoek.assert(!this._stateDefinitions[name], 'State already defined:', name); - Hoek.assert(!options || !options.encoding || ['base64json', 'base64', 'form', 'iron', 'none'].indexOf(options.encoding) !== -1, 'Bad encoding'); + if (options) { + Schema.assert('state', options, name); + } this._stateDefinitions[name] = Hoek.applyToDefaults(Defaults.state, options || {}); }; diff --git a/lib/state.js b/lib/state.js index 3521a9d65..3be2bf996 100755 --- a/lib/state.js +++ b/lib/state.js @@ -39,7 +39,7 @@ internals.validateRx = { exports.parseCookies = function (request, next) { - var prepare = function () { + var parse = function () { request.state = {}; @@ -49,12 +49,10 @@ exports.parseCookies = function (request, next) { return next(); } - header(cookies); - }; - - var header = function (cookies) { + // Parse header var state = {}; + var names = []; var verify = cookies.replace(internals.parseRx, function ($0, $1, $2, $3) { var name = $1; @@ -69,6 +67,7 @@ exports.parseCookies = function (request, next) { } else { state[name] = value; + names.push(name); } return ''; @@ -82,43 +81,37 @@ exports.parseCookies = function (request, next) { return; // shouldStop calls next() } - // Validate cookie + // Parse cookies + + var parsed = {}; + Async.forEachSeries(names, function (name, nextName) { + + var value = state[name]; + var definition = request.server._stateDefinitions[name]; - if (request.server.settings.state.cookies.strictHeader) { - var names = Object.keys(state); - for (var i = 0, il = names.length; i < il; ++i) { - var name = names[i]; + // Validate cookie + + var strict = (definition && definition.strictHeader !== undefined ? definition.strictHeader + : request.server.settings.state.cookies.strictHeader); + if (strict) { if (!name.match(internals.validateRx.nameRx.strict)) { - if (shouldStop(cookies, name)) { + if (shouldStop(cookies, name, definition)) { return; // shouldStop calls next() } } var values = [].concat(state[name]); for (var v = 0, vl = values.length; v < vl; ++v) { - var value = values[v]; - if (!value.match(internals.validateRx.valueRx.strict)) { - if (shouldStop(cookies, name)) { + if (!values[v].match(internals.validateRx.valueRx.strict)) { + if (shouldStop(cookies, name, definition)) { return; // shouldStop calls next() } } } } - } - parse(state); - }; - - var parse = function (state) { - - var parsed = {}; - - var names = Object.keys(state); - Async.forEachSeries(names, function (name, nextName) { - - var value = state[name]; + // Check cookie format - var definition = request.server._stateDefinitions[name]; if (!definition || !definition.encoding) { @@ -132,7 +125,7 @@ exports.parseCookies = function (request, next) { unsign(name, value, definition, function (err, unsigned) { if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) { + if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { return; // shouldStop calls next() } @@ -142,7 +135,7 @@ exports.parseCookies = function (request, next) { decode(unsigned, definition, function (err, result) { if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) { + if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { return; // shouldStop calls next() } @@ -165,7 +158,7 @@ exports.parseCookies = function (request, next) { unsign(name, arrayValue, definition, function (err, unsigned) { if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) { + if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { return; // shouldStop calls next() } @@ -175,7 +168,7 @@ exports.parseCookies = function (request, next) { decode(unsigned, definition, function (err, result) { if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) { + if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { return; // shouldStop calls next() } @@ -288,23 +281,25 @@ exports.parseCookies = function (request, next) { return innerNext(null, result); }; - var shouldStop = function (error, name) { - - if (request.server.settings.state.cookies.clearInvalid && - name) { + var shouldStop = function (error, name, definition) { + var clearInvalid = (definition && definition.clearInvalid !== undefined ? definition.clearInvalid + : request.server.settings.state.cookies.clearInvalid); + if (clearInvalid && name) { request._clearState(name); } // failAction: 'error', 'log', 'ignore' - if (request.server.settings.state.cookies.failAction === 'log' || - request.server.settings.state.cookies.failAction === 'error') { + var failAction = (definition && definition.failAction !== undefined ? definition.failAction + : request.server.settings.state.cookies.failAction); + if (failAction === 'log' || + failAction === 'error') { request.log(['hapi', 'state', 'error'], error); } - if (request.server.settings.state.cookies.failAction === 'error') { + if (failAction === 'error') { next(Boom.badRequest('Bad cookie ' + (name ? 'value: ' + Hoek.escapeHtml(name) : 'header'))); return true; } @@ -312,7 +307,7 @@ exports.parseCookies = function (request, next) { return false; }; - prepare(); + parse(); }; diff --git a/test/state.js b/test/state.js index b866ea110..976ac253b 100755 --- a/test/state.js +++ b/test/state.js @@ -173,6 +173,7 @@ describe('State', function () { pass('key=Fe26.2**f3fc42242467f7a97c042be866a32c1e7645045c2cc085124eadc66d25fc8395*URXpH8k-R0d4O5bnY23fRQ*uq9rd8ZzdjZqUrq9P2Ci0yZ-EEUikGzxTLn6QTcJ0bc**3880c0ac8bab054f529afec8660ebbbbc8050e192e39e5d622e7ac312b9860d0*r_g7N9kJYqXDrFlvOnuKpfpEWwrJLOKMXEI43LAGeFg', { key: { a: 1, b: 2, c: 3 } }, null, { key: { encoding: 'iron', password: 'password', iron: Iron.defaults } }); pass('sid=a=1&b=2&c=3%20x.2d75635d74c1a987f84f3ee7f3113b9a2ff71f89d6692b1089f19d5d11d140f8*xGhc6WvkE55V-TzucCl0NVFmbijeCwgs5Hf5tAVbSUo', { sid: { a: '1', b: '2', c: '3 x' } }, null, { sid: { encoding: 'form', sign: { password: 'password' } } }); pass('sid=a=1&b=2&c=3%20x.2d75635d74c1a987f84f3ee7f3113b9a2ff71f89d6692b1089f19d5d11d140f8*xGhc6WvkE55V-TzucCl0NVFmbijeCwgs5Hf5tAVbSUo', { sid: { a: '1', b: '2', c: '3 x' } }, null, { sid: { encoding: 'form', sign: { password: 'password', integrity: Iron.defaults.integrity } } }); + pass('a="1', { a: '"1' }, null, { a: { strictHeader: false } }); var loose = Hoek.clone(Defaults.server.state); loose.cookies.strictHeader = false; @@ -258,6 +259,35 @@ describe('State', function () { clearInvalid.cookies.clearInvalid = true; fail('sid=a=1&b=2&c=3%20x', clearInvalid, { sid: { encoding: 'form', sign: { password: 'password' } } }); }); + + it('uses cookie failAction override', function (done) { + + var server = new Hapi.Server(); + server.state('test', { failAction: 'log' }); + server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(request.state.test); } }); + + server.inject({ method: 'GET', url: '/', headers: { cookie: 'test="a' } }, function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('"a'); + done(); + }); + }); + + it('uses cookie clearInvalid override', function (done) { + + var server = new Hapi.Server(); + server.state('test', { clearInvalid: true, failAction: 'ignore', encoding: 'base64json' }); + server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(request.state.test); } }); + + server.inject({ method: 'GET', url: '/', headers: { cookie: 'test=a' } }, function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal(null); + expect(res.headers['set-cookie'][0]).to.equal('test=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT'); + done(); + }); + }); }); describe('#generateSetCookieHeader', function () { From 830ddf6e07e23206aab1e26d060801b227612162 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Wed, 4 Jun 2014 23:15:21 -0700 Subject: [PATCH 27/33] auth non-error log. Closes #1687 --- lib/auth.js | 5 +++-- test/payload.js | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/auth.js b/lib/auth.js index a4952016d..fc9f6ad0f 100755 --- a/lib/auth.js +++ b/lib/auth.js @@ -185,7 +185,7 @@ internals.Auth.prototype._authenticate = function (request, next) { }); }; - var validate = function (err, strategy, result) { + var validate = function (err, strategy, result) { // err can be Boom, Error, or a valid response object if (!strategy) { return next(err); @@ -204,7 +204,8 @@ internals.Auth.prototype._authenticate = function (request, next) { request.log(['hapi', 'auth', 'error', strategy].concat(result.log.tags), result.log.data); } else { - request.log(['hapi', 'auth', 'error', 'unauthenticated'], (err.isBoom ? err : err.statusCode)); + var tags = err.isBoom ? ['hapi', 'auth', 'error', 'unauthenticated'] : ['hapi', 'auth', 'response', 'unauthenticated']; + request.log(tags, (err.isBoom ? err : err.statusCode)); } if (err.isMissing) { diff --git a/test/payload.js b/test/payload.js index 0c0906a6c..542847515 100755 --- a/test/payload.js +++ b/test/payload.js @@ -586,11 +586,26 @@ describe('Payload', function () { } }; - Nipple.post('http://localhost:' + server.info.port + '/?x=3', options, function (err, res, body) { + var options = { + hostname: 'localhost', + port: server.info.port, + path: '/?x=3', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': '1' + } + }; + + var req = Http.request(options, function (res) { }); - expect(err.message).to.equal('Client request error: socket hang up'); + req.once('error', function (err) { + + expect(err.message).to.equal('socket hang up'); done(); }); + + req.end('{ "key": "value" }'); }); }); From 842ce8329031c283afc617fbe355468c82913519 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Wed, 4 Jun 2014 23:37:48 -0700 Subject: [PATCH 28/33] reply.redirect(). Closes #1688 --- docs/Reference.md | 29 ++++++++++++++++++++++++----- lib/directory.js | 2 +- lib/handler.js | 5 +++++ test/proxy.js | 6 +++--- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index d94d1ede5..9015ebeb3 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -47,6 +47,7 @@ - [`reply.view(template, [context, [options]])`](#replyviewtemplate-context-options) - [`reply.close([options])`](#replycloseoptions) - [`reply.proxy(options)`](#replyproxyoptions) + - [`reply.redirect(location)`](#replyredirectlocation) - [Response object](#response-object) - [Response events](#response-events) - [`Hapi.error`](#hapierror) @@ -1632,7 +1633,7 @@ var pre = function (request, reply) { ### `reply([result])` _Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, -`reply.close()`, or `reply.proxy()` is called._ +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ Concludes the handler activity by returning control over to the router where: @@ -1673,7 +1674,7 @@ Note that if `result` is a `Stream` with a `statusCode` property, that status co ### `reply.file(path, [options])` _Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, -`reply.close()`, or `reply.proxy()` is called._ +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ Transmits a file from the file system. The 'Content-Type' header defaults to the matching mime type based on filename extension.: @@ -1700,7 +1701,7 @@ var handler = function (request, reply) { ### `reply.view(template, [context, [options]])` _Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, -`reply.close()`, or `reply.proxy()` is called._ +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ Concludes the handler activity by returning control over to the router with a templatized view response where: @@ -1753,7 +1754,7 @@ server.route({ method: 'GET', path: '/', handler: handler }); ### `reply.close([options])` _Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, -`reply.close()`, or `reply.proxy()` is called._ +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ Concludes the handler activity by returning control over to the router and informing the router that a response has already been sent back directly via `request.raw.res` and that no further response action is needed. Supports the following optional options: @@ -1766,7 +1767,7 @@ The [response flow control rules](#flow-control) **do not** apply. ### `reply.proxy(options)` _Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, -`reply.close()`, or `reply.proxy()` is called._ +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ Proxies the request to an upstream endpoint where: - `options` - an object including the same keys and restrictions defined by the [route `proxy` handler options](#route.config.proxy). @@ -1782,6 +1783,24 @@ var handler = function (request, reply) { }; ``` +### `reply.redirect(location)` + +_Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ + +Redirects the client to the specified location. Same as calling `reply().redirect(location)`. + +Returns a response object. + +The [response flow control rules](#flow-control) apply. + +```javascript +var handler = function (request, reply) { + + reply.redirect('http://example.com'); +}; +``` + ## Response object Every response includes the following properties: diff --git a/lib/directory.js b/lib/directory.js index 545b62526..79e6e2d5c 100755 --- a/lib/directory.js +++ b/lib/directory.js @@ -140,7 +140,7 @@ exports.handler = function (route, options) { !request.server.settings.router.stripTrailingSlash && !hasTrailingSlash) { - return reply().redirect(resource + '/'); + return reply.redirect(resource + '/'); } if (!index) { diff --git a/lib/handler.js b/lib/handler.js index 7f1b633e5..baf59e14f 100755 --- a/lib/handler.js +++ b/lib/handler.js @@ -142,6 +142,11 @@ exports.replyInterface = function (request, finalize, base) { request._clearState(name); }; + reply.redirect = function (location) { + + return internals.wrap('', request, finalize).redirect(location); + } + return reply; }; diff --git a/test/proxy.js b/test/proxy.js index 97f731e57..9496f3b27 100755 --- a/test/proxy.js +++ b/test/proxy.js @@ -765,7 +765,7 @@ describe('Proxy', function () { var redirectHandler = function (request, reply) { - reply().redirect('/redirect?x=1'); + reply.redirect('/redirect?x=1'); }; var upstream = new Hapi.Server(0); @@ -833,7 +833,7 @@ describe('Proxy', function () { var redirectHandler = function (request, reply) { - reply().redirect('/profile'); + reply.redirect('/profile'); }; var profile = function (request, reply) { @@ -894,7 +894,7 @@ describe('Proxy', function () { it('redirects to a post endpoint with stream', function (done) { var upstream = new Hapi.Server(0); - upstream.route({ method: 'POST', path: '/post1', handler: function (request, reply) { reply().redirect('/post2').rewritable(false); } }); + upstream.route({ method: 'POST', path: '/post1', handler: function (request, reply) { reply.redirect('/post2').rewritable(false); } }); upstream.route({ method: 'POST', path: '/post2', handler: function (request, reply) { reply(request.payload); } }); upstream.start(function () { From f9e739ed51a54030ecf4a499ee88c0ba52db3b4f Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Thu, 5 Jun 2014 12:23:59 -0700 Subject: [PATCH 29/33] lowercase hapi --- docs/Reference.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index 9015ebeb3..0014200b6 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -104,7 +104,7 @@ - [`Hapi.state`](#hapistate) - [`prepareValue(name, value, options, callback)`](#preparevaluename-value-options-callback) - [`Hapi.version`](#hapiversion) -- [Hapi CLI](#hapi-cli) +- [hapi CLI](#hapi-cli) ## `Hapi.Server` @@ -2920,7 +2920,7 @@ var Hapi = require('hapi'); console.log(Hapi.version); ``` -## `Hapi CLI` +## `hapi CLI` The **hapi** command line interface allows a pack of servers to be composed and started from a configuration file only from the command line. When installing **hapi** with the global flag the **hapi** binary script will be From 76779aab64993a753d7cf574481f47df0b580446 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sat, 7 Jun 2014 16:06:00 -0700 Subject: [PATCH 30/33] auth.test(). Closes #1692 --- docs/Reference.md | 10 ++++++++++ lib/auth.js | 15 +++++++++++++++ test/auth.js | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/docs/Reference.md b/docs/Reference.md index 0014200b6..8e4bb4169 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -23,6 +23,7 @@ - [`server.cache(name, options)`](#servercachename-options) - [`server.auth.scheme(name, scheme)`](#serverauthschemename-scheme) - [`server.auth.strategy(name, scheme, [mode], [options])`](#serverauthstrategyname-scheme-mode-options) + - [`server.auth.test(strategy, request, next)`](#serverauthteststrategy-request-next) - [`server.ext(event, method, [options])`](#serverextevent-method-options) - [Request lifecycle](#request-lifecycle) - [`server.method(name, fn, [options])`](#servermethodname-fn-options) @@ -1044,6 +1045,15 @@ Registers an authentication strategy where: (`'required'`, `'optional'`, `'try'`). Defaults to `false`. - `options` - scheme options based on the scheme requirements. +#### `server.auth.test(strategy, request, next)` + +Tests a request against an authentication strategy where: +- `strategy` - the strategy name registered with `server.auth.strategy()`. +- `request` - the request object. The request route authentication configuration is not used. +- `next` - the callback function with signature `function(err, credentials)` where: + - `err` - the error if authentication failed. + - `credentials` - the authentication credentials object if authentication was successful. + #### `server.ext(event, method, [options])` Registers an extension function in one of the available [extension points](#request-lifecycle) where: diff --git a/lib/auth.js b/lib/auth.js index fc9f6ad0f..fa721b271 100755 --- a/lib/auth.js +++ b/lib/auth.js @@ -56,6 +56,21 @@ internals.Auth.prototype.strategy = function (name, scheme /*, mode, options */) }; +internals.Auth.prototype.test = function (name, request, next) { + + Hoek.assert(name, 'Missing authentication strategy name'); + var strategy = this._strategies[name]; + Hoek.assert(strategy, 'Unknown authentication strategy:', name); + + var root = function (err, result) { + + return (err ? reply._root(err) : next(err, result.credentials)); + }; + + var reply = Handler.replyInterface(request, next, root); + strategy.authenticate.call(null, request, reply); +}; + internals.Auth.prototype._setupRoute = function (options) { var self = this; diff --git a/test/auth.js b/test/auth.js index aa235c652..ffdc15c5c 100755 --- a/test/auth.js +++ b/test/auth.js @@ -786,6 +786,40 @@ describe('Auth', function () { }); }); }); + + it('tests a request', function (done) { + + var handler = function (request, reply) { + + request.server.auth.test('default', request, function (err, credentials) { + + if (err) { + return reply({ status: false }); + } + + return reply({ status: true, user: credentials.name }); + }); + }; + + var server = new Hapi.Server(); + server.auth.scheme('custom', internals.implementation); + server.auth.strategy('default', 'custom', { users: { steve: { name: 'steve' } } }); + server.route({ method: 'GET', path: '/', handler: handler }); + + server.inject('/', function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result.status).to.equal(false); + + server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result.status).to.equal(true); + expect(res.result.user).to.equal('steve'); + done(); + }); + }); + }); }); From e5df652b9fede134a82e9adb55bf706b70321dfa Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sat, 7 Jun 2014 21:24:36 -0700 Subject: [PATCH 31/33] Auth defaults. Closes #1693 --- docs/Reference.md | 13 +++- lib/auth.js | 98 +++++++++++++------------ lib/route.js | 2 +- lib/schema.js | 41 ++++++----- test/auth.js | 179 +++++++++++++++++++++++++++++++++++----------- 5 files changed, 224 insertions(+), 109 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index 8e4bb4169..44b8422fe 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -23,6 +23,7 @@ - [`server.cache(name, options)`](#servercachename-options) - [`server.auth.scheme(name, scheme)`](#serverauthschemename-scheme) - [`server.auth.strategy(name, scheme, [mode], [options])`](#serverauthstrategyname-scheme-mode-options) + - [`server.auth.default(options)`](#serverauthdefault-options) - [`server.auth.test(strategy, request, next)`](#serverauthteststrategy-request-next) - [`server.ext(event, method, [options])`](#serverextevent-method-options) - [Request lifecycle](#request-lifecycle) @@ -590,10 +591,9 @@ The following options are available when adding a route: - `expiresAt` - time of day expressed in 24h notation using the 'MM:HH' format, at which point all cache records for the route expire. Cannot be used together with `expiresIn`. - - `auth` - authentication configuration. Value can be: + - `auth` - authentication configuration. Value can be: + - `false` to disable authentication if a default strategy is set. - a string with the name of an authentication strategy registered with `server.auth.strategy()`. - - a boolean where `false` means no authentication, and `true` sets to the default authentication strategy which is available only - when a single strategy is configured. - an object with: - `mode` - the authentication mode. Defaults to `'required'` if a server authentication strategy is configured, otherwise defaults to no authentication. Available values: @@ -1045,6 +1045,13 @@ Registers an authentication strategy where: (`'required'`, `'optional'`, `'try'`). Defaults to `false`. - `options` - scheme options based on the scheme requirements. +#### `server.auth.default(options)` + +Sets a default startegy which is applied to every route. The default does not apply when the route config specifies `auth` as `false`, +or has an authentication strategy configured. Otherwise, the route authentication config is applied to the defaults. The function requires: +- `options` - a string with the default strategy name or an object with a specified strategy or strategies using the same format as the + [route `auth` handler options](#route.config.auth). + #### `server.auth.test(strategy, request, next)` Tests a request against an authentication strategy where: diff --git a/lib/auth.js b/lib/auth.js index fa721b271..957143ea0 100755 --- a/lib/auth.js +++ b/lib/auth.js @@ -4,6 +4,7 @@ var Boom = require('boom'); var Hoek = require('hoek'); var Semver = require('semver'); var Handler = require('./handler'); +var Schema = require('./schema'); // Declare internals @@ -16,10 +17,7 @@ exports = module.exports = internals.Auth = function (server) { this.server = server; this._schemes = {}; this._strategies = {}; - this._defaultStrategy = { // Strategy used as default if route has no auth settings - name: null, - mode: 'required' - }; + this._defaultStrategy = null; // Strategy used as default if route has no auth settings }; @@ -44,18 +42,40 @@ internals.Auth.prototype.strategy = function (name, scheme /*, mode, options */) Hoek.assert(!this._strategies[name], 'Authentication strategy name already exists'); Hoek.assert(scheme, 'Authentication strategy', name, 'missing scheme'); Hoek.assert(this._schemes[scheme], 'Authentication strategy', name, 'uses unknown scheme:', scheme); - Hoek.assert(!mode || !this._defaultStrategy.name, 'Cannot set default required strategy more than once:', name, '- already set to:', this._defaultStrategy); this._strategies[name] = this._schemes[scheme](this.server, options); if (mode) { - this._defaultStrategy.name = name; - this._defaultStrategy.mode = (typeof mode === 'string' ? mode : 'required'); - Hoek.assert(['required', 'optional', 'try'].indexOf(this._defaultStrategy.mode) !== -1, 'Unknown default authentication mode:', this._defaultStrategy.mode); + this.default({ strategies: [name], mode: mode === true ? 'required' : mode }); } }; +internals.Auth.prototype.default = function (options) { + + Schema.assert('auth', options, 'default strategy'); + Hoek.assert(!this._defaultStrategy, 'Cannot set default strategy more than once'); + + var settings = Hoek.clone(options); // options can be reused + + if (typeof settings === 'string') { + settings = { + strategies: [settings], + mode: 'required' + }; + } + + if (settings.strategy) { + settings.strategies = [settings.strategy]; + delete settings.strategy; + } + + Hoek.assert(settings.strategies && settings.strategies.length, 'Default authentication strategy missing strategy name'); + + this._defaultStrategy = settings; +}; + + internals.Auth.prototype.test = function (name, request, next) { Hoek.assert(name, 'Missing authentication strategy name'); @@ -71,7 +91,8 @@ internals.Auth.prototype.test = function (name, request, next) { strategy.authenticate.call(null, request, reply); }; -internals.Auth.prototype._setupRoute = function (options) { + +internals.Auth.prototype._setupRoute = function (options, path) { var self = this; @@ -79,38 +100,36 @@ internals.Auth.prototype._setupRoute = function (options) { return options; // Preseve the difference between undefined and false } - var names = Object.keys(this._strategies); - var defaultName = (names.length === 1 ? names[0] : null); - if (typeof options === 'string') { options = { strategies: [options] }; } - else if (options === true) { - Hoek.assert(defaultName, 'Cannot set auth to true when more than one configured'); - options = { strategies: [defaultName] }; + + if (options.strategy) { + options.strategies = [options.strategy]; + delete options.strategy; } - options.mode = options.mode || 'required'; - Hoek.assert(['required', 'optional', 'try'].indexOf(options.mode) !== -1, 'Unknown authentication mode:', options.mode); + if (!options.strategies) { + Hoek.assert(this._defaultStrategy, 'Route missing authentication strategy and no default defined:', path); + options = Hoek.applyToDefaults(this._defaultStrategy, options); + } - Hoek.assert(!options.entity || ['user', 'app', 'any'].indexOf(options.entity) !== -1, 'Unknown authentication entity type:', options.entity); - Hoek.assert(!options.payload || ['required', 'optional'].indexOf(options.payload) !== -1, 'Unknown authentication payload mode:', options.entity); - Hoek.assert(!(options.strategy && options.strategies), 'Route can only have a auth.strategy or auth.strategies (or use the default) but not both'); - Hoek.assert(!options.strategies || options.strategies.length, 'Cannot have empty auth.strategies array'); - Hoek.assert(options.strategies || options.strategy || defaultName, 'Cannot use default strategy when none or more than one configured'); - options.strategies = options.strategies || [options.strategy || defaultName]; - delete options.strategy; + Hoek.assert(options.strategies.length, 'Route missing authentication strategy:', path); + options.mode = options.mode || 'required'; options.payload = options.payload || false; - var hasAuthenticatePayload = false; - options.strategies.forEach(function (strategy) { - Hoek.assert(self._strategies[strategy], 'Unknown authentication strategy:', strategy); - hasAuthenticatePayload = hasAuthenticatePayload || typeof self._strategies[strategy].payload === 'function'; - Hoek.assert(options.payload !== 'required' || hasAuthenticatePayload, 'Payload validation can only be required when all strategies support it'); - }); + if (options.payload) { + var hasAuthenticatePayload = false; + options.strategies.forEach(function (strategy) { + + Hoek.assert(self._strategies[strategy], 'Unknown authentication strategy:', strategy, 'in path:', path); + hasAuthenticatePayload = hasAuthenticatePayload || typeof self._strategies[strategy].payload === 'function'; + Hoek.assert(options.payload !== 'required' || hasAuthenticatePayload, 'Payload validation can only be required when all strategies support it in path:', path); + }); - Hoek.assert(!options.payload || hasAuthenticatePayload, 'Payload authentication requires at least one strategy with payload support'); + Hoek.assert(hasAuthenticatePayload, 'Payload authentication requires at least one strategy with payload support in path:', path); + } return options; }; @@ -118,24 +137,11 @@ internals.Auth.prototype._setupRoute = function (options) { internals.Auth.prototype._routeConfig = function (request) { - if (request.route.auth) { - return request.route.auth; - } - - if (request.route.auth === false || - request.route.auth === null) { - + if (request.route.auth === false) { return false; } - if (this._defaultStrategy.name) { - return { - mode: this._defaultStrategy.mode, - strategies: [this._defaultStrategy.name] - }; - } - - return false; + return request.route.auth || this._defaultStrategy; }; diff --git a/lib/route.js b/lib/route.js index 7a82bc525..929f1160d 100755 --- a/lib/route.js +++ b/lib/route.js @@ -115,7 +115,7 @@ exports = module.exports = internals.Route = function (options, server, env) { // Authentication configuration - this.settings.auth = this.server.auth._setupRoute(this.settings.auth); + this.settings.auth = this.server.auth._setupRoute(this.settings.auth, options.path); // Cache diff --git a/lib/schema.js b/lib/schema.js index 5dd185ab9..7cc824dd3 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -174,6 +174,27 @@ internals.pre = [ ]; +internals.auth = Joi.alternatives([ + Joi.string(), + Joi.object({ + mode: Joi.string().valid('required', 'optional', 'try'), + scope: [ + Joi.string(), + Joi.array() + ], + tos: Joi.string().allow(false), + entity: Joi.string().valid('user', 'app', 'any'), + strategy: Joi.string(), + strategies: Joi.array().min(1), + payload: [ + Joi.string().valid('required', 'optional'), + Joi.boolean() + ] + }) + .without('strategy', 'strategies') +]); + + internals.routeConfig = Joi.object({ pre: Joi.array().includes(internals.pre.concat(Joi.array().includes(internals.pre).min(1))), handler: [ @@ -194,25 +215,7 @@ internals.routeConfig = Joi.object({ uploads: Joi.string(), failAction: Joi.string().valid('error', 'log', 'ignore') }), - auth: [ - Joi.object({ - mode: Joi.string().valid('required', 'optional', 'try'), - scope: [ - Joi.string(), - Joi.array() - ], - tos: Joi.string().allow(false), - entity: Joi.string(), - strategy: Joi.string(), - strategies: Joi.array(), - payload: [ - Joi.string(), - Joi.boolean() - ] - }), - Joi.boolean().allow(false), - Joi.string() - ], + auth: internals.auth.allow(false), validate: Joi.object({ headers: Joi.alternatives(Joi.object(), Joi.func()).allow(null, false, true), params: Joi.alternatives(Joi.object(), Joi.func()).allow(null, false, true), diff --git a/test/auth.js b/test/auth.js index ffdc15c5c..681f0a59e 100755 --- a/test/auth.js +++ b/test/auth.js @@ -41,6 +41,71 @@ describe('Auth', function () { }); }); + it('sets default', function (done) { + + var server = new Hapi.Server(); + server.auth.scheme('custom', internals.implementation); + server.auth.strategy('default', 'custom', { users: { steve: {} } }); + server.auth.default('default'); + server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(request.auth.credentials.user); } }); + + server.inject('/', function (res) { + + expect(res.statusCode).to.equal(401); + + server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) { + + expect(res.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('sets default with object', function (done) { + + var server = new Hapi.Server(); + server.auth.scheme('custom', internals.implementation); + server.auth.strategy('default', 'custom', { users: { steve: {} } }); + server.auth.default({ strategy: 'default' }); + server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(request.auth.credentials.user); } }); + + server.inject('/', function (res) { + + expect(res.statusCode).to.equal(401); + + server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) { + + expect(res.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('throws when setting default twice', function (done) { + + var server = new Hapi.Server(); + server.auth.scheme('custom', internals.implementation); + server.auth.strategy('default', 'custom', { users: { steve: {} } }); + expect(function () { + + server.auth.default('default'); + server.auth.default('default'); + }).to.throw('Cannot set default strategy more than once'); + done(); + }); + + it('throws when setting default without strategy', function (done) { + + var server = new Hapi.Server(); + server.auth.scheme('custom', internals.implementation); + server.auth.strategy('default', 'custom', { users: { steve: {} } }); + expect(function () { + + server.auth.default({ mode: 'required' }); + }).to.throw('Default authentication strategy missing strategy name'); + done(); + }); + it('authenticates using multiple strategies', function (done) { var server = new Hapi.Server(); @@ -135,44 +200,6 @@ describe('Auth', function () { }); }); - it('enables individual route authentications', function (done) { - - var server = new Hapi.Server(); - server.auth.scheme('custom', internals.implementation); - server.auth.strategy('default', 'custom', { users: { steve: {} } }); - server.route({ - method: 'GET', - path: '/1', - config: { - handler: function (request, reply) { reply(request.auth.credentials.user); }, - auth: true - } - }); - server.route({ - method: 'GET', - path: '/2', - config: { - handler: function (request, reply) { reply('ok'); } - } - }); - - server.inject('/1', function (res) { - - expect(res.statusCode).to.equal(401); - - server.inject({ url: '/1', headers: { authorization: 'Custom steve' } }, function (res) { - - expect(res.statusCode).to.equal(200); - - server.inject('/2', function (res) { - - expect(res.statusCode).to.equal(200); - done(); - }); - }); - }); - }); - it('tries to authenticate a request', function (done) { var server = new Hapi.Server(); @@ -577,6 +604,78 @@ describe('Auth', function () { }); }); + it('throws when strategy does not support payload authentication', function (done) { + + var server = new Hapi.Server(); + var implementation = function () { return { authenticate: internals.implementation.authenticate } }; + + server.auth.scheme('custom', implementation); + server.auth.strategy('default', 'custom', true, {}); + expect(function () { + + server.route({ + method: 'POST', + path: '/', + config: { + handler: function (request, reply) { reply(request.auth.credentials.user); }, + auth: { + payload: 'required' + } + } + }); + }).to.throw('Payload validation can only be required when all strategies support it in path: /'); + done(); + }); + + it('throws when no strategy supports optional payload authentication', function (done) { + + var server = new Hapi.Server(); + var implementation = function () { return { authenticate: internals.implementation.authenticate } }; + + server.auth.scheme('custom', implementation); + server.auth.strategy('default', 'custom', true, {}); + expect(function () { + + server.route({ + method: 'POST', + path: '/', + config: { + handler: function (request, reply) { reply(request.auth.credentials.user); }, + auth: { + payload: 'optional' + } + } + }); + }).to.throw('Payload authentication requires at least one strategy with payload support in path: /'); + done(); + }); + + it('allows one strategy to supports optional payload authentication while another does not', function (done) { + + var server = new Hapi.Server(); + var implementation = function () { return { authenticate: internals.implementation.authenticate } }; + + server.auth.scheme('custom1', implementation); + server.auth.scheme('custom2', internals.implementation); + server.auth.strategy('default1', 'custom1', {}); + server.auth.strategy('default2', 'custom2', {}); + expect(function () { + + server.route({ + method: 'POST', + path: '/', + config: { + handler: function (request, reply) { reply(request.auth.credentials.user); }, + auth: { + strategies: ['default2', 'default1'], + payload: 'optional' + } + } + }); + }).to.not.throw(); + done(); + }); + it('skips request payload by default', function (done) { var server = new Hapi.Server(); @@ -601,7 +700,7 @@ describe('Auth', function () { var server = new Hapi.Server(); server.auth.scheme('custom', internals.implementation); - server.auth.strategy('default', 'custom', { users: { skip: { } } }); + server.auth.strategy('default', 'custom', true, { users: { skip: { } } }); server.route({ method: 'POST', path: '/', @@ -765,7 +864,7 @@ describe('Auth', function () { var server = new Hapi.Server(); server.auth.scheme('custom', internals.implementation); - server.auth.strategy('default', 'custom', { users: { steve: {} } }); + server.auth.strategy('default', 'custom', true, { users: { steve: {} } }); var handler = function (request, reply) { From 4f90baa8943caa1bc0a315427278387a6ef74826 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sat, 7 Jun 2014 22:00:32 -0700 Subject: [PATCH 32/33] Minor error tweaks --- lib/auth.js | 2 +- lib/handler.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/auth.js b/lib/auth.js index 957143ea0..855409ca7 100755 --- a/lib/auth.js +++ b/lib/auth.js @@ -242,7 +242,7 @@ internals.Auth.prototype._authenticate = function (request, next) { request.auth.strategy = strategy; request.auth.credentials = result.credentials; request.auth.artifacts = result.artifacts; - request.log(['hapi', 'auth', 'error', 'unauthenticated', 'try'], err); + request.log(['hapi', 'auth', 'unauthenticated', 'try'], err); return next(); } diff --git a/lib/handler.js b/lib/handler.js index baf59e14f..3ca2a7bb0 100755 --- a/lib/handler.js +++ b/lib/handler.js @@ -74,7 +74,7 @@ internals.handler = function (request, callback) { // Check for Error result if (response.isBoom) { - request.log(['hapi', 'handler', 'error'], { msec: timer.elapsed() }); + request.log(['hapi', 'handler', 'error'], { msec: timer.elapsed(), error: response.message }); return callback(response); } From e8e5050393421a85edaa3b14010438ade5253fa8 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sat, 7 Jun 2014 23:02:33 -0700 Subject: [PATCH 33/33] Remove unused dep --- lib/utils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/utils.js b/lib/utils.js index acb1a273e..5229cf2bf 100755 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,7 +2,6 @@ var Crypto = require('crypto'); var Path = require('path'); -var Hoek = require('hoek'); // Declare internals