diff --git a/.travis.yml b/.travis.yml index b9879cc..c576a02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: node_js node_js: + - "8" - "6" - - "5" - - "4" diff --git a/index.js b/index.js index c5ad907..46d934e 100644 --- a/index.js +++ b/index.js @@ -1,151 +1,11 @@ 'use strict'; -const fs = require('fs'); -const glob = require('glob'); -const path = require('path'); -const _ = require('lodash'); -const async = require('async'); -const defaults = { - path: `${process.cwd()}/routes`, - base: '/', - prefix: '', - verbose: false, - autoLoad: true -}; +exports.routeLoader = require('./lib/routeLoader'); exports.register = (server, options, next) => { exports.routeLoader(server, options, next, true); }; -// the 'base' of the path for all routes defined in a given file. -// eg "/api/folder1/myfile.js" will return "/api/folder/myfile/", -const getRoutePathBase = (options, fileName) => { - // gets relative path, minus absolute path specifier: - const segment = path.dirname(fileName.replace(options.path, '')); - let base = options.base ? options.base : defaults.base; - if (segment === '.' || !segment) { - return base; - } - if (!_.endsWith(options.base, '/')) { - base += '/'; - } - return `${base}${segment}`; -}; - -// get the full route path: -const getCompletePath = (options, fileName, originalPath) => { - const prefix = options.prefix ? options.prefix : defaults.prefix; - // if the originalPath started with a slash just return it, there is no basePath: - if (_.startsWith(originalPath, '/')) { - let resultPath = `${prefix}${originalPath}`; - return resultPath; - } - // otherwise add the basePath to the returnPath: - const basePath = getRoutePathBase(options, fileName); - let returnPath = path.join(basePath, originalPath ? originalPath : '').replace(new RegExp('(\\\\)', 'g'), '/'); - returnPath = `${prefix}${returnPath}`; - // if there's a trailing slash, make sure it should be there: - if (_.endsWith(returnPath, '/') && (!_.endsWith(basePath, '/') || basePath === '/') && returnPath !== '/') { - if (!_.endsWith(originalPath, '/')) { - returnPath = returnPath.substr(0, returnPath.length - 1); - } - } - if (_.startsWith(returnPath, '//')) { - returnPath = returnPath.replace('//', '/'); - } - return returnPath; -}; - -exports.routeLoader = (server, options, next) => { - const load = (loadOptions, loadDone) => { - const stub = () => {}; - loadDone = loadDone || stub; - const settings = _.clone(loadOptions); - _.defaults(settings, defaults); - // the main flow is here: - async.auto({ - // confirm that settings.path exists and is a directory: - confirmDirectoryExists: (done) => { - fs.exists(settings.path, (exists) => { - if (!exists) { - server.log(['hapi-route-loader', 'warning'], { message: 'path doesnt exist', path: settings.path }); - return done(); - } - fs.stat(settings.path, (err, stat) => { - if (err) { - return done(err); - } - if (!stat.isDirectory()) { - server.log(['hapi-route-loader', 'warning'], { message: 'path not a directory', path: settings.path }); - return done(`path ${settings.path} not a directory`); - } - return done(); - }); - }); - }, - // get the list of all matching route-containing files - files: ['confirmDirectoryExists', (results, done) => { - glob('**/*.js', { - cwd: settings.path - }, (globErr, files) => { - if (globErr) { - return done(globErr); - } - done(null, files); - }); - }], - // for each filename, get a list of configured routes defined by it - configureAllRoutes: ['files', (results, done) => { - const routeConfigs = {}; - results.files.forEach((fileName) => { - const fileRouteList = []; - const moduleRoutes = require(path.join(settings.path, fileName)); - _.forIn(moduleRoutes, (originalRouteConfig) => { - if (typeof originalRouteConfig === 'function') { - originalRouteConfig = originalRouteConfig(server, settings); - } - const processedRouteConfig = _.clone(originalRouteConfig); - // set up the route's 'config' option: - if (options.routeConfig) { - if (originalRouteConfig.config) { - processedRouteConfig.config = _.defaults(options.routeConfig, originalRouteConfig.config); - } else { - processedRouteConfig.config = options.routeConfig; - } - } - // set up the route's 'path' option: - processedRouteConfig.path = getCompletePath(options, fileName, originalRouteConfig.path); - fileRouteList.push(processedRouteConfig); - }); - routeConfigs[fileName] = fileRouteList; - }); - done(null, routeConfigs); - }], - // register all routes with server.route: - registerAllRoutes: ['configureAllRoutes', (results, done) => { - Object.keys(results.configureAllRoutes).forEach((fileName) => { - results.configureAllRoutes[fileName].forEach((routeConfig) => { - if (options.verbose) { - server.log(['debug', 'hapi-route-loader'], { msg: 'registering', data: routeConfig }); - } - server.route(routeConfig); - }); - }); - done(); - }] - }, (err2) => { - if (err2) { - server.log(['hapi-route-loader', 'error'], err2); - } - return loadDone(err2); - }); - }; - if (options.autoLoad === false) { - return next(); - } - load(options, next); -}; - exports.register.attributes = { pkg: require('./package.json') }; diff --git a/lib/configureRoute.js b/lib/configureRoute.js new file mode 100644 index 0000000..04b25b4 --- /dev/null +++ b/lib/configureRoute.js @@ -0,0 +1,16 @@ +const getCompletePath = require('./getCompletePath'); + +module.exports = (settings, fileName, routeConfig) => { + const finalRouteConfig = Object.assign({}, routeConfig); + // set up the route's 'config' option: + if (settings.routeConfig) { + if (routeConfig.config) { + finalRouteConfig.config = Object.assign({}, routeConfig.config, settings.routeConfig); + } else { + finalRouteConfig.config = settings.routeConfig; + } + } + // set up the route's 'path' option: + finalRouteConfig.path = getCompletePath(settings, fileName, routeConfig.path); + return finalRouteConfig; +}; diff --git a/lib/defaults.js b/lib/defaults.js new file mode 100644 index 0000000..8d435c8 --- /dev/null +++ b/lib/defaults.js @@ -0,0 +1,7 @@ +module.exports = { + path: `${process.cwd()}/routes`, + base: '/', + prefix: '', + verbose: false, + autoLoad: true +}; diff --git a/lib/getCompletePath.js b/lib/getCompletePath.js new file mode 100644 index 0000000..c87c190 --- /dev/null +++ b/lib/getCompletePath.js @@ -0,0 +1,26 @@ +const path = require('path'); +const defaults = require('./defaults'); +const getRoutePathBase = require('./getRoutePathBase'); + +// get the full route path: +module.exports = (options, fileName, originalPath) => { + const prefix = options.prefix ? options.prefix : defaults.prefix; + // if the originalPath started with a slash just return it, there is no basePath: + if (originalPath && originalPath.startsWith('/')) { + return `${prefix}${originalPath}`; + } + // otherwise add the basePath to the returnPath: + const basePath = getRoutePathBase(options, fileName); + let returnPath = path.join(basePath, originalPath || '').replace(new RegExp('(\\\\)', 'g'), '/'); + returnPath = `${prefix}${returnPath}`; + // if there's a trailing slash, make sure it should be there: + if (returnPath.endsWith('/') && (!basePath.endsWith('/') || basePath === '/') && returnPath !== '/') { + if (!originalPath || !originalPath.endsWith('/')) { + returnPath = returnPath.substr(0, returnPath.length - 1); + } + } + if (returnPath.endsWith('//')) { + returnPath = returnPath.replace('//', '/'); + } + return returnPath; +}; diff --git a/lib/getRoutePathBase.js b/lib/getRoutePathBase.js new file mode 100644 index 0000000..ca90bec --- /dev/null +++ b/lib/getRoutePathBase.js @@ -0,0 +1,19 @@ +const path = require('path'); +const defaults = require('./defaults'); + +// the 'base' of the path for all routes defined in a given file. +// eg "/api/folder1/myfile.js" will return "/api/folder/myfile/", +const getRoutePathBase = (options, fileName) => { + // gets relative path, minus absolute path specifier: + const segment = path.dirname(fileName.replace(options.path, '')); + let base = options.base ? options.base : defaults.base; + if (segment === '.' || !segment) { + return base; + } + if (options.base && !options.base.endsWith('/')) { + base += '/'; + } + return `${base}${segment}`; +}; + +module.exports = getRoutePathBase; diff --git a/lib/getRoutesFromFiles.js b/lib/getRoutesFromFiles.js new file mode 100644 index 0000000..b62f380 --- /dev/null +++ b/lib/getRoutesFromFiles.js @@ -0,0 +1,18 @@ +'use strict'; +const path = require('path'); +const configureRoute = require('./configureRoute'); + +module.exports = (server, settings, files, done) => { + const configuredRoutes = []; + files.forEach((fileName) => { + const moduleRoutes = require(path.join(settings.path, fileName)); + Object.keys(moduleRoutes).forEach((originalRouteConfigKey) => { + let originalRouteConfig = moduleRoutes[originalRouteConfigKey]; + if (typeof originalRouteConfig === 'function') { + originalRouteConfig = originalRouteConfig(server, settings); + } + configuredRoutes.push(configureRoute(settings, fileName, originalRouteConfig)); + }); + }); + return done(null, configuredRoutes); +}; diff --git a/lib/routeLoader.js b/lib/routeLoader.js new file mode 100644 index 0000000..ee48b62 --- /dev/null +++ b/lib/routeLoader.js @@ -0,0 +1,44 @@ +const fs = require('fs'); +const glob = require('glob'); +const async = require('async'); +const defaults = require('./defaults'); +const validateRoutesDirectory = require('./validateRoutesDirectory'); +const getRoutesFromFiles = require('./getRoutesFromFiles'); + +module.exports = (server, options, next) => { + const load = (loadOptions, loadDone) => { + const stub = () => {}; + loadDone = loadDone || stub; + const settings = Object.assign({}, defaults, loadOptions); + // the main flow is here: + async.autoInject({ + // confirm that settings.path exists and is a directory: + confirmDirectoryExists(done) { + validateRoutesDirectory(server, settings, done); + }, + // get the list of all matching route-containing files + files(confirmDirectoryExists, done) { + glob('**/*.js', { + cwd: settings.path + }, done); + }, + configuredRoutes(files, done) { + getRoutesFromFiles(server, settings, files, done); + }, + // register all routes with server.route: + registerAllRoutes: (configuredRoutes, done) => { + configuredRoutes.forEach((routeConfig) => { + if (options.verbose) { + server.log(['debug', 'hapi-route-loader'], { msg: 'registering', data: routeConfig }); + } + server.route(routeConfig); + }); + done(); + } + }, loadDone); + }; + if (options.autoLoad === false) { + return next(); + } + load(options, next); +}; diff --git a/lib/validateRoutesDirectory.js b/lib/validateRoutesDirectory.js new file mode 100644 index 0000000..a5f5bfa --- /dev/null +++ b/lib/validateRoutesDirectory.js @@ -0,0 +1,22 @@ +const fs = require('fs'); + +const validateRoutesDirectory = (server, settings, done) => { + fs.exists(settings.path, (exists) => { + if (!exists) { + server.log(['hapi-route-loader', 'warning'], { message: 'path doesnt exist', path: settings.path }); + return done(); + } + fs.stat(settings.path, (err, stat) => { + if (err) { + return done(err); + } + if (!stat.isDirectory()) { + server.log(['hapi-route-loader', 'warning'], { message: 'path not a directory', path: settings.path }); + return done(`path ${settings.path} not a directory`); + } + return done(); + }); + }); +}; + +module.exports = validateRoutesDirectory; diff --git a/package.json b/package.json index 1550bd1..76fd1bb 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,7 @@ "homepage": "https://github.com/firstandthird/hapi-route-loader#readme", "dependencies": { "async": "^2.1.2", - "glob": "^7.1.1", - "lodash": "^4.17.2" + "glob": "^7.1.1" }, "devDependencies": { "chai": "^3.4.1", diff --git a/test/route.methods.js b/test/route.methods.js new file mode 100644 index 0000000..bcfd142 --- /dev/null +++ b/test/route.methods.js @@ -0,0 +1,148 @@ +const chai = require('chai'); +const assert = chai.assert; +const validateRoutesDirectory = require('../lib/validateRoutesDirectory'); +const getRoutePathBase = require('../lib/getRoutePathBase'); +const getCompletePath = require('../lib/getCompletePath'); +const getRoutesFromFiles = require('../lib/getRoutesFromFiles'); +const configureRoute = require('../lib/configureRoute'); + +describe('validateRoutesDirectory', () => { + it('logs if a directory does not exist', (done) => { + const server = { + log(tags, msg) { + assert(msg.message === 'path doesnt exist'); + } + }; + const settings = { + path: 'nonono' + }; + validateRoutesDirectory(server, settings, (outcome) => { + done(); + }); + }); + it('logs if a directory is actually a file', (done) => { + const server = { + log(tags, msg) { + assert(msg.message === 'path not a directory'); + } + }; + const settings = { + path: __filename + }; + validateRoutesDirectory(server, settings, (outcome) => { + assert(outcome.indexOf('not a directory')); + done(); + }); + }); + it('is fine with valid directories', (done) => { + const server = {}; + const settings = { + path: __dirname + }; + validateRoutesDirectory(server, settings, (outcome) => { + assert(outcome === undefined); + done(); + }); + }); +}); + +describe('getCompletePath ', () => { + it('get route when originalPath starts with /', (done) => { + const result = getCompletePath({}, 'routes/test/testerson.js', '/originalPath'); + assert(result === '/originalPath'); + done(); + }); + + it('get route when originalPath does not start with /', (done) => { + const result = getCompletePath({}, 'routes/test/testerson.js', 'originalPath'); + assert(result === '/routes/test/originalPath'); + done(); + }); + + it('get route when originalPath ends with /', (done) => { + const result = getCompletePath({}, 'routes/test/testerson.js', 'originalPath/'); + assert(result === '/routes/test/originalPath/'); + done(); + }); +}); + +describe('getRoutePathBase', () => { + it('turn a file path into the base of the route name', (done) => { + const result = getRoutePathBase({}, 'routes/test/testerson.js'); + assert(result === '/routes/test'); + done(); + }); + + it('appends the file base to the route name', (done) => { + const result = getRoutePathBase({ base: 'base' }, 'routes/test/testerson.js'); + assert(result === 'base/routes/test'); + done(); + }); +}); + +describe('getRoutesFromFiles', () => { + it('gets routes from a file when provided as a module export:', (done) => { + const settings = { + path: `${__dirname}/routes` + }; + const server = { + value: 'hello world' + }; + getRoutesFromFiles(server, settings, ['route.js'], (err, configuredRoutes) => { + assert(err === null); + assert(configuredRoutes.length === 8); + configuredRoutes.forEach((route) => { + assert(typeof route === 'object'); + assert(route.path !== undefined); + assert(route.method !== undefined); + assert(route.handler !== undefined); + }); + done(); + }); + }); + it('gets routes from a file when provided as a function:', (done) => { + const settings = { + path: `${__dirname}/functionRoutes`, + functionTestThingy: 'test' + }; + const server = { + version: 'hello world' + }; + getRoutesFromFiles(server, settings, ['function.js'], (err, configuredRoutes) => { + assert(err === null); + assert(configuredRoutes.length === 1); + const route = configuredRoutes[0]; + assert(typeof route === 'object'); + assert(route.path !== undefined); + assert(route.method !== undefined); + assert(route.handler !== undefined); + route.handler({}, (value) => { + assert.equal(value, `${server.version},${settings.functionTestThingy}`); + done(); + }); + }); + }); +}); + +describe('configureRoute', () => { + it('configures a route from its definition', (done) => { + const settings = { + path: `${__dirname}/routes`, + base: 'first', + routeConfig: { + strategy: 'none' + } + }; + const routeConfig = { + path: 'crooked', + method: 'also crooked', + handler(request, reply) { + return reply(); + } + }; + const result = configureRoute(settings, 'route.js', routeConfig); + assert(result.path === 'first/crooked'); + assert(result.config.strategy === 'none'); + done(); + }); +}); diff --git a/test/route.tests.js b/test/route.tests.js index 11b3cdc..be78e31 100644 --- a/test/route.tests.js +++ b/test/route.tests.js @@ -407,6 +407,7 @@ describe('hapi-route-loader lets you specify routeConfig object for all routes', }); }); }); + describe('hapi-route-loader deeply nested route', () => { let server; it(" file: '/routes/api/test/test.js' => '/api/test'", (done) => {