From 390a6ba9119cc24bea993a0f2795cd5d1468a53f Mon Sep 17 00:00:00 2001 From: Paul Bakker Date: Mon, 21 May 2018 19:00:42 -0700 Subject: [PATCH 1/2] new: hot reload functionallity (#13) --- lib/install.js | 52 +++++++++++++++++++++++++++++++++++++++++++++++++- lib/schemas.js | 3 +++ package.json | 3 ++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/lib/install.js b/lib/install.js index bc00769..b986b8a 100644 --- a/lib/install.js +++ b/lib/install.js @@ -6,6 +6,9 @@ var _ = require('lodash'); var assert = require('assert-plus'); var vasync = require('vasync'); var verror = require('verror'); +var compose = require('restify').helpers.compose; +var bunyan = require('bunyan'); +var LOG; /** * Install the enroute routes into the restify server. @@ -23,6 +26,12 @@ function install(opts, cb) { assert.object(opts.server, 'opts.server'); assert.string(opts.basePath, 'opts.basePath'); + if (typeof opts.log !== 'undefined') { + LOG = opts.log.child({ component: 'enroute' }); + } else { + LOG = bunyan.createLogger({name: 'enroute'}); + } + vasync.pipeline({arg: {}, funcs: [ // Read the routes from disk and parse them as functions function getRoutes(ctx, cb1) { @@ -32,6 +41,15 @@ function install(opts, cb) { return cb1(); }); + if (opts.enroute.hotReload) { + if (typeof opts.basePath !== 'undefined') { + LOG.info({basedir: opts.basePath}, + 'Hot reloading of routes is enabled for base dir'); + } else { + LOG.info('Hot reloading of routes is enabled.'); + } + } + // go through each of the route names _.forEach(opts.enroute.routes, function (methods, routeName) { // go through each of the HTTP methods @@ -45,7 +63,12 @@ function install(opts, cb) { route = { name: routeName, method: method, - func: require(sourceFile) + func: opts.enroute.hotReload + ? function (req, resp, callback) { + reloadProxy(sourceFile, req, + resp, callback, opts); + } + : require(sourceFile) }; } catch (e) { return cb1(new verror.VError({ @@ -89,4 +112,31 @@ function install(opts, cb) { }); } +function reloadProxy(sourceFile, req, resp, callback, opts) { + if (typeof opts.basePath !== 'undefined') { + //Delete code loaded from a specific base dir + Object.keys(require.cache).forEach(function (cacheKey) { + if (cacheKey.indexOf(opts.basePath) !== -1) { + delete require.cache[cacheKey]; + } + }); + } else { + //Delete all cached entries + Object.keys(require.cache).forEach(function (cacheKey) { + delete require.cache[cacheKey]; + }); + } + + try { + var handlers = require(sourceFile); + var handlerChain = compose(handlers); + + handlerChain(req, resp, callback); + } catch (e) { + LOG.error('Uncaught error in route:'); + LOG.error(e); + callback(e); + } +} + module.exports = install; diff --git a/lib/schemas.js b/lib/schemas.js index 9b6a801..9021487 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -99,6 +99,9 @@ module.exports = { }, schemaVersion: { type: 'number' + }, + hotReload: { + type: 'boolean' } }, additionalProperties: false, diff --git a/package.json b/package.json index dc25362..3a36987 100644 --- a/package.json +++ b/package.json @@ -37,14 +37,15 @@ "jscs": "^3.0.7", "mocha": "^3.1.2", "nsp": "^2.6.2", - "restify": "^7.0.0", "restify-clients": "^1.4.0", "uuid": "^2.0.3" }, "dependencies": { "ajv": "^4.8.0", "assert-plus": "^1.0.0", + "bunyan": "^1.8.12", "lodash": "^4.16.4", + "restify": "^7.2.0", "vasync": "^1.6.4", "verror": "^1.6.0" } From 18a843b11db436169afc87a9222e299894cdc859 Mon Sep 17 00:00:00 2001 From: Alex Liu Date: Mon, 21 May 2018 19:33:45 -0700 Subject: [PATCH 2/2] fix: pr comments --- lib/install.js | 103 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 21 deletions(-) diff --git a/lib/install.js b/lib/install.js index b986b8a..f2f79d1 100644 --- a/lib/install.js +++ b/lib/install.js @@ -8,13 +8,17 @@ var vasync = require('vasync'); var verror = require('verror'); var compose = require('restify').helpers.compose; var bunyan = require('bunyan'); + +// local globals var LOG; + /** * Install the enroute routes into the restify server. * * @param {object} opts Options object. * @param {object} opts.enroute The parsed enroute configuration. + * @param {object} opts.log an optional logger * @param {object} opts.server The restify server. * @param {string} opts.basePath The basepath to resolve source files. * @param {function} cb The callback. @@ -22,14 +26,21 @@ var LOG; */ function install(opts, cb) { assert.object(opts, 'opts'); + assert.optionalObject(opts.log, 'opts.log'); assert.object(opts.enroute, 'opts.enroute'); assert.object(opts.server, 'opts.server'); assert.string(opts.basePath, 'opts.basePath'); - if (typeof opts.log !== 'undefined') { - LOG = opts.log.child({ component: 'enroute' }); + var log; + + if (opts.log) { + log = opts.log.child({ component : 'enroute' }); } else { - LOG = bunyan.createLogger({name: 'enroute'}); + // only create default logger if one wasn't passed in. + if (!LOG) { + LOG = bunyan.createLogger({ name: 'enroute' }); + } + log = LOG; } vasync.pipeline({arg: {}, funcs: [ @@ -43,10 +54,10 @@ function install(opts, cb) { if (opts.enroute.hotReload) { if (typeof opts.basePath !== 'undefined') { - LOG.info({basedir: opts.basePath}, - 'Hot reloading of routes is enabled for base dir'); + log.info({ basedir: opts.basePath }, + 'hot reloading of routes is enabled for base dir'); } else { - LOG.info('Hot reloading of routes is enabled.'); + log.info('hot reloading of routes is enabled.'); } } @@ -60,25 +71,34 @@ function install(opts, cb) { var route; try { + var func = (opts.enroute.hotReload) ? + // restify middleware wrapper for hot reload + function enrouteHotReloadProxy(req, res, next) { + return reloadProxy({ + basePath: opts.basePath, + method: method, + req: req, + res: res, + routeName: routeName, + sourceFile: sourceFile + }, next); + } : require(sourceFile); + route = { name: routeName, method: method, - func: opts.enroute.hotReload - ? function (req, resp, callback) { - reloadProxy(sourceFile, req, - resp, callback, opts); - } - : require(sourceFile) + func: func }; } catch (e) { return cb1(new verror.VError({ + name: 'EnrouteRequireError', cause: e, info: { file: sourceFile, route: routeName, method: method } - }, 'route function is invalid')); + }, 'failed to require file, possible syntax error')); } // if HTTP method is 'delete', since restify uses 'del' // instead of 'delete', change it to 'del' @@ -112,7 +132,34 @@ function install(opts, cb) { }); } -function reloadProxy(sourceFile, req, resp, callback, opts) { + +/** + * using the restify handler composer, create a middleware on the fly that + * re-requires the file on every load executes it. + * @private + * @function reloadProxy + * @param {Object} opts an options object + * @param {String} opts.basePath The basepath to resolve source files. + * @param {String} opts.method http verb + * @param {Object} opts.log the enroute logger + * @param {Object} opts.req the request object + * @param {Object} opts.res the response object + * @param {String} opts.routeName the name of the restify route + * @param {String} opts.sourceFile the response object + * @param {Function} cb callback fn + * @returns {undefined} + */ +function reloadProxy(opts, cb) { + assert.object(opts, 'opts'); + assert.object(opts.basePath, 'opts.basePath'); + assert.string(opts.method, 'opts.method'); + assert.object(opts.log, 'opts.log'); + assert.object(opts.req, 'opts.req'); + assert.object(opts.res, 'opts.res'); + assert.string(opts.routeName, 'opts.routeName'); + assert.string(opts.sourceFile, 'opts.sourceFile'); + assert.func(cb, 'cb'); + if (typeof opts.basePath !== 'undefined') { //Delete code loaded from a specific base dir Object.keys(require.cache).forEach(function (cacheKey) { @@ -127,16 +174,30 @@ function reloadProxy(sourceFile, req, resp, callback, opts) { }); } - try { - var handlers = require(sourceFile); - var handlerChain = compose(handlers); + var handlers; - handlerChain(req, resp, callback); + try { + handlers = require(opts.sourceFile); } catch (e) { - LOG.error('Uncaught error in route:'); - LOG.error(e); - callback(e); + var err = new verror.VError({ + name: 'EnrouteRequireError', + cause: e, + info: { + file: opts.sourceFile, + route: opts.routeName, + method: opts.method + } + }, 'failed to require file, possible syntax error'); + + // now that the chain has failed, send back the require error. + return cb(err); } + + // if no require error, execute the handler chain. any errors that occur at + // runtime should be a runtime exception. + var handlerChain = compose(handlers); + return handlerChain(opts.req, opts.res, cb); } + module.exports = install;