diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..0dc8dc2 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,5 @@ +engines: + eslint: + enabled: true + duplication: + enabled: false diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 4ebc8ae..0000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -coverage diff --git a/.travis.yml b/.travis.yml index ba5724f..f87f358 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ node_js: - 4 - 5 - 6 + - 7 compiler: - gcc env: @@ -41,6 +42,10 @@ script: - npm test - npm run ci +after_script: + - npm install -g codeclimate-test-reporter + - codeclimate-test-reporter < coverage/lcov.info + notifications: email: false webhooks: diff --git a/README.md b/README.md index 6799839..d5b5e35 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,14 @@ capabilities and allow developers to leverage existing ecosystem tools through a simple and well-defined API. New features, behavior, APIs, and other functionality can be added to the Trails framework through Trailpacks. -Out of the box, Trails includes a small suite of trailpacks: +Many Trails installations will include some of the following Trailpacks: -- [core](https://github.com/trailsjs/trailpack-core) - [router](https://github.com/trailsjs/trailpack-router) - [repl](https://github.com/trailsjs/trailpack-repl) - [hapi](https://github.com/trailsjs/trailpack-hapi) +- [express](https://github.com/trailsjs/trailpack-express) - [waterline](https://github.com/trailsjs/trailpack-waterline) +- [knex](https://github.com/trailsjs/trailpack-knex) ## Compatibility @@ -86,7 +87,9 @@ Out of the box, Trails includes a small suite of trailpacks: - [Getting Started with Trails.js](https://www.youtube.com/watch?v=AbSp8jqFDAY) #### Support -- [Gitter chat room](https://gitter.im/trailsjs/trails) +- [Stackoverflow](http://stackoverflow.com/questions/tagged/trailsjs) +- [Live Gitter Chat](https://gitter.im/trailsjs/trails) +- [Twitter](https://twitter.com/trailsjs) ## FAQ diff --git a/archetype/api/services/DefaultService.js b/archetype/api/services/DefaultService.js index 564b054..3482c67 100644 --- a/archetype/api/services/DefaultService.js +++ b/archetype/api/services/DefaultService.js @@ -1,6 +1,6 @@ 'use strict' -const Service = require('trails-service') +const Service = require('trails/service') /** * @module DefaultService diff --git a/archetype/config/i18n.js b/archetype/config/i18n.js index 2e67af5..34f0ce8 100644 --- a/archetype/config/i18n.js +++ b/archetype/config/i18n.js @@ -5,9 +5,7 @@ * If your app will touch people from all over the world, i18n (or internationalization) * may be an important part of your international strategy. * - * * @see http://trailsjs.io/doc/config/i18n - * */ 'use strict' diff --git a/archetype/config/main.js b/archetype/config/main.js index e2c184f..543f41c 100644 --- a/archetype/config/main.js +++ b/archetype/config/main.js @@ -16,7 +16,6 @@ module.exports = { * requirements. */ packs: [ - require('trailpack-core'), require('trailpack-repl'), require('trailpack-router'), require('<%- trailpacks %>') diff --git a/archetype/package.json b/archetype/package.json index 11bbc25..008bfc6 100644 --- a/archetype/package.json +++ b/archetype/package.json @@ -6,19 +6,14 @@ ], "main": "index.js", "dependencies": { - "trailpack-core": "^1.0.1", - "trailpack-repl": "^1.1.0", - "trailpack-router": "^1.0.8", - "trails": "^1.1.0", - "trails-controller": "^1.0.0-beta-2", - "trails-model": "^1.0.0-beta-2", - "trails-policy": "^1.0.1", - "trails-service": "1.0.0-beta-2", - "winston": "^2.2" + "trailpack-repl": "v2-latest", + "trailpack-router": "v2-latest", + "trails": "v2-latest", + "winston": "^2.3" }, "devDependencies": { "eslint": "^2.11", - "eslint-config-trails": "latest", + "eslint-config-trails": "^2", "mocha": "^2.5", "supertest": "^1.2" }, diff --git a/archetype/server.js b/archetype/server.js index 991ab17..af7e8d9 100644 --- a/archetype/server.js +++ b/archetype/server.js @@ -6,8 +6,8 @@ 'use strict' -const app = require('./') const TrailsApp = require('trails') +const app = require('./') const server = new TrailsApp(app) server.start().catch(err => server.stop(err)) diff --git a/controller.js b/controller.js new file mode 100644 index 0000000..dfc3ff7 --- /dev/null +++ b/controller.js @@ -0,0 +1 @@ +module.exports = require('trails-controller') diff --git a/index.js b/index.js index 4284d7c..e3c3a96 100644 --- a/index.js +++ b/index.js @@ -1,17 +1,24 @@ /*eslint no-console: 0 */ 'use strict' -const events = require('events') +const EventEmitter = require('events').EventEmitter const lib = require('./lib') +const i18next = require('i18next') +const NOOP = function () { } + +// inject Error and Resource types into the global namespace +lib.Core.assignGlobals() /** * The Trails Application. Merges the configuration and API resources * loads Trailpacks, initializes logging and event listeners. */ -module.exports = class TrailsApp extends events.EventEmitter { +module.exports = class TrailsApp extends EventEmitter { /** * @param pkg The application package.json + * @param app.api The application api (api/ folder) + * @param app.config The application configuration (config/ folder) * * Initialize the Trails Application and its EventEmitter parentclass. Set * some necessary default configuration. @@ -19,19 +26,26 @@ module.exports = class TrailsApp extends events.EventEmitter { constructor (app) { super() - if (!process.env.NODE_ENV) { - process.env.NODE_ENV = 'development' + if (!app) { + throw new RangeError('No app definition provided to Trails constructor') } if (!app.pkg) { throw new lib.Errors.PackageNotDefinedError() } + if (!app.api) { + throw new lib.Errors.ApiNotDefinedError() + } - lib.Trails.validateConfig(app.config) + if (!process.env.NODE_ENV) { + process.env.NODE_ENV = 'development' + } + + const processEnv = Object.freeze(JSON.parse(JSON.stringify(process.env))) Object.defineProperties(this, { env: { enumerable: false, - value: Object.freeze(JSON.parse(JSON.stringify(process.env))) + value: processEnv }, pkg: { enumerable: false, @@ -44,8 +58,9 @@ module.exports = class TrailsApp extends events.EventEmitter { value: process.versions }, config: { - value: lib.Trails.buildConfig(app.config), - configurable: true + value: new lib.Configuration(app.config, processEnv), + configurable: true, + writable: false }, api: { value: app.api, @@ -66,7 +81,7 @@ module.exports = class TrailsApp extends events.EventEmitter { }, loadedModules: { enumerable: false, - value: lib.Trails.getExternalModules(this.pkg) + value: lib.Core.getExternalModules(this.pkg) }, bound: { enumerable: false, @@ -87,34 +102,73 @@ module.exports = class TrailsApp extends events.EventEmitter { enumerable: false, writable: true, value: { } + }, + models: { + enumerable: true, + writable: false, + value: { } + }, + services: { + enumerable: true, + writable: false, + value: { } + }, + controllers: { + enumerable: true, + writable: false, + value: { } + }, + policies: { + enumerable: true, + writable: false, + value: { } + }, + translate: { + enumerable: false, + writable: true } }) this.setMaxListeners(this.config.main.maxListeners) - this.config.main.packs.forEach(Pack => new Pack(this)) - delete this.config.env // Delete env config, now it has been merge + + // instatiate trailpacks + this.config.main.packs.forEach(Pack => { + try { + new Pack(this) + } + catch (e) { + console.error('Error loading Trailpack') + console.error(e) + } + }) + this.loadedPacks = Object.keys(this.packs).map(name => this.packs[name]) + + // bind resource methods to 'app' + Object.assign(this.models, lib.Core.bindMethods(this, 'models')) + Object.assign(this.services, lib.Core.bindMethods(this, 'services')) + Object.assign(this.controllers, lib.Core.bindMethods(this, 'controllers')) + Object.assign(this.policies, lib.Core.bindMethods(this, 'policies')) } /** - * Start the App. Load all Trailpacks. The "api" property is required, here, - * if not provided to the constructor. + * Start the App. Load all Trailpacks. * - * @param app.api The application api (api/ folder) - * @param app.config The application configuration (config/ folder) * @return Promise */ - start (app) { - if (!this.api && !(app && app.api)) { - throw new lib.Errors.ApiNotDefinedError() - } - this.api || (this.api = app && app.api) - - this.loadedPacks = Object.keys(this.packs).map(name => this.packs[name]) - lib.Trails.bindEvents(this) + start () { + lib.Core.bindListeners(this) lib.Trailpack.bindTrailpackPhaseListeners(this, this.loadedPacks) lib.Trailpack.bindTrailpackMethodListeners(this, this.loadedPacks) - this.emit('trails:start') + // initialize i18n + i18next.init(this.config.i18n, (err, t) => { + if (err) { + this.log.error('Problem loading i18n:', err) + } + + this.translate = t + this.emit('trails:start') + }) return this.after('trails:ready') .then(() => { @@ -138,7 +192,7 @@ module.exports = class TrailsApp extends events.EventEmitter { } this.emit('trails:stop') - lib.Trails.unbindEvents(this) + lib.Core.unbindListeners(this) return Promise.all( this.loadedPacks.map(pack => { @@ -149,6 +203,10 @@ module.exports = class TrailsApp extends events.EventEmitter { this.log.debug('All trailpacks unloaded. Done.') return this }) + .catch(err => { + console.error(err) + return this + }) } /** @@ -161,34 +219,51 @@ module.exports = class TrailsApp extends events.EventEmitter { } /** - * Extend the once emiter reader for accept multi valid events + * Resolve Promise once ANY of the events in the list have emitted. Also + * accepts a callback. + * @return Promise */ onceAny (events, handler) { - const self = this - - if (!events) - return - if (!Array.isArray(events)) + handler || (handler = NOOP) + if (!Array.isArray(events)) { events = [events] + } - function cb (e) { - self.removeListener(e, cb) - handler.apply(this, Array.prototype.slice.call(arguments, 0)) + let resolveCallback + const handlerWrapper = function () { + handler.apply(null, arguments) + return arguments } - events.forEach(e => { - this.addListener(e, cb) + return Promise.race(events.map(eventName => { + return new Promise(resolve => { + resolveCallback = resolve + this.once(eventName, resolveCallback) + }) + })) + .then(handlerWrapper) + .then(args => { + events.forEach(eventName => this.removeListener(eventName, resolveCallback)) + return args }) } /** - * Resolve Promise once all events in the list have emitted + * Resolve Promise once all events in the list have emitted. Also accepts + * a callback. * @return Promise */ - after (events) { + after (events, handler) { + handler || (handler = NOOP) if (!Array.isArray(events)) { events = [ events ] } + + const handlerWrapper = (args) => { + handler(args) + return args + } + return Promise.all(events.map(eventName => { return new Promise(resolve => { if (eventName instanceof Array){ @@ -199,6 +274,36 @@ module.exports = class TrailsApp extends events.EventEmitter { } }) })) + .then(handlerWrapper) + } + + /** + * Prevent changes to the app configuration + */ + freezeConfig () { + this.config.freeze(this.loadedModules) + } + + /** + * Allow changes to the app configuration + */ + unfreezeConfig () { + Object.defineProperties(this, { + config: { + value: new lib.Configuration(this.config.unfreeze(), this.env), + configurable: true + } + }) + } + + /** + * Create any configured paths which may not already exist. + */ + createPaths () { + if (this.config.main.createPaths === false) { + this.log.warn('createPaths is disabled. Configured paths will not be created') + } + return lib.Core.createDefaultPaths(this) } /** @@ -208,4 +313,12 @@ module.exports = class TrailsApp extends events.EventEmitter { get log () { return this.config.log.logger } + + /** + * Expose the i18n translator on the app object. Internationalization can be + * configured in config.i18n + */ + get __ () { + return this.translate + } } diff --git a/lib/ConfigProxy.js b/lib/ConfigProxy.js new file mode 100644 index 0000000..ca345e8 --- /dev/null +++ b/lib/ConfigProxy.js @@ -0,0 +1,5 @@ +/** + * TODO in v3.0 when node4 support is dropped + */ +module.exports = class ConfigProxy { +} diff --git a/lib/Configuration.js b/lib/Configuration.js new file mode 100644 index 0000000..6bb7729 --- /dev/null +++ b/lib/Configuration.js @@ -0,0 +1,186 @@ +/*eslint no-console: 0 */ +'use strict' + +const _ = require('lodash') +const path = require('path') +const joi = require('joi') +const schemas = require('./schemas') + +module.exports = class Configuration extends Map { + constructor (configTree, processEnv) { + super() + + this.modules = [ ] + this.immutable = false + this.env = processEnv + this.tree = Configuration.buildConfig(configTree, processEnv.NODE_ENV) + + Configuration.validateConfig(this.tree) + + // this looks somewhat strange; I'd like to use a Proxy here, but Node 4 + // does not support it. These properties will be exposed via a Proxy + // for v3.0 when Node 4 support can be dropped. + Object.assign(this, this.tree) + } + + /** + * @override + */ + get (key) { + return _.get(this, key) + } + + /** + * @override + */ + set (key, val) { + return _.set(this, key, val) + } + + /** + * @override + */ + freeze (modules) { + this.immutable = true + this.modules = modules + Configuration.freezeConfig(this, modules) + } + + unfreeze () { + return Configuration.unfreezeConfig(this, this.modules) + } + + /** + * Copy and merge the provided configuration into a new object, decorated with + * necessary default and environment-specific values. + */ + static buildConfig (initialConfig, nodeEnv) { + const root = path.resolve(path.dirname(require.main.filename)) + const temp = path.resolve(root, '.tmp') + const envConfig = initialConfig.env && initialConfig.env[nodeEnv] + + const configTemplate = { + main: { + maxListeners: 128, + packs: [ ], + paths: { + root: root, + temp: temp, + sockets: path.resolve(temp, 'sockets'), + logs: path.resolve(temp, 'log') + }, + timeouts: { + start: 10000, + stop: 10000 + }, + freezeConfig: true, + createPaths: true + }, + log: { }, + motd: require('./motd') + } + + const mergedConfig = _.merge(configTemplate, initialConfig, (envConfig || { })) + mergedConfig.env = nodeEnv + + return mergedConfig + } + + /** + * Validate the structure and prerequisites of the configuration object. Throw + * an Error if invalid; invalid configurations are unrecoverable and require + * that programmer fix them. + */ + static validateConfig (config) { + if (!config || !config.main) { + throw new ConfigNotDefinedError() + } + + if (!config.log || !config.log.logger) { + throw new LoggerNotDefinedError() + } + + const nestedEnvs = Configuration.getNestedEnv(config) + if (nestedEnvs.length) { + throw new ConfigValueError('Environment configs cannot contain an "env" property') + } + + if (config.env && config.env.env) { + throw new ConfigValueError('config.env cannot contain an "env" property') + } + + const result = joi.validate(config, schemas.config) + if (result.error) { + console.error(result.error) + throw new Error(`Project Configuration Error: ${result.error}`) + } + } + + /** + * Check to see if the user defined a property config.env[env].env + */ + static getNestedEnv (config) { + const env = (config && config.env) + const nestedEnvs = Object.keys(env || { }).filter(key => { + return !!env[key].env + }) + + return nestedEnvs + } + + /** + * Deep freeze application config object. Exceptions are made for required + * modules that are listed as dependencies in the application's + * package definition (package.json). Trails takes a firm stance that + * configuration should be modified only by the developer, the environment, + * and the trailpack configuration phase -- never by the application itself + * during runtime. + * + * @param config the configuration object to be frozen. + * @param [pkg] the package definition to use for exceptions. optional. + */ + static freezeConfig (config, modules) { + _.each(config, (prop, name) => { + if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { + return + } + + const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) + if (ignoreModule) { + return + } + Configuration.freezeConfig(prop, modules) + }) + + Object.freeze(config) + } + + /** + * Copy the configuration into a normal, unfrozen object + */ + static unfreezeConfig (config, modules) { + const unfreeze = (target, source) => { + const propNames = Object.getOwnPropertyNames(source) + + propNames.forEach(name => { + const prop = source[name] + + if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { + target[name] = prop + return + } + + const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) + if (ignoreModule) { + return + } + + target[name] = { } + unfreeze(target[name], prop) + }) + + return target + } + return unfreeze({ }, config) + } +} diff --git a/lib/core.js b/lib/core.js new file mode 100644 index 0000000..02a2e9b --- /dev/null +++ b/lib/core.js @@ -0,0 +1,196 @@ +/*eslint no-console: 0 */ +'use strict' + +const path = require('path') +const fs = require('fs') +const _ = require('lodash') +const mkdirp = require('mkdirp') +const lib = require('./') + +const Core = module.exports = { + + reservedMethods: [ + 'app', + 'api', + 'log', + '__', + 'constructor', + 'undefined', + 'methods', + 'config', + 'schema' + ], + + globals: Object.freeze(Object.assign({ + Service: require('../service'), + Controller: require('../controller'), + Policy: require('../policy'), + Model: require('../model') + }, lib.Errors)), + + globalPropertyOptions: Object.freeze({ + writable: false, + enumerable: false, + configurable: false + }), + + /** + * Prepare the global namespace with required Trails types. Ignore identical + * values already present; fail on non-matching values. + * + * @throw NamespaceConflictError + */ + assignGlobals () { + _.each(lib.Core.globals, (type, name) => { + if (global[name] === type) return + if (global[name] && global[name] !== type) { + throw new lib.Errors.NamespaceConflictError(name, Object.keys(lib.Core.globals)) + } + const descriptor = Object.assign({ value: type }, lib.Core.globalPropertyOptions) + Object.defineProperty(global, name, descriptor) + }) + }, + + /** + * Bind the context of API resource methods. + */ + bindMethods (app, resource) { + return _.mapValues(app.api[resource], (Resource, resourceName) => { + if (_.isPlainObject(Resource)) { + throw new Error(`${resourceName} should be a class. It is a regular object`) + } + + const obj = new Resource(app) + + obj.methods = Core.getClassMethods(obj) + _.forEach(obj.methods, method => { + obj[method] = obj[method].bind(obj) + }) + return obj + }) + }, + + /** + * Traverse protoype chain and aggregate all class method names + */ + getClassMethods (obj) { + const props = [ ] + const objectRoot = new Object() + + while (!obj.isPrototypeOf(objectRoot)) { + Object.getOwnPropertyNames(obj).forEach(prop => { + if (props.indexOf(prop) === -1 && + !_.includes(Core.reservedMethods, prop) && + _.isFunction(obj[prop])) { + + props.push(prop) + } + }) + obj = Object.getPrototypeOf(obj) + } + + return props + }, + + /** + * Create configured paths if they don't exist + */ + createDefaultPaths (app) { + const paths = app.config.main.paths + + return Promise.all(_.map(paths, (dir, pathName) => { + return new Promise((resolve, reject) => { + fs.stat(dir, (err, stats) => { + resolve({ err, stats }) + }) + }) + .then(result => { + const stats = result.stats + + if (stats && !stats.isDirectory()) { + app.log.error('The configured path "', pathName, '" is not a directory.') + app.log.error('config.main.paths should only contain paths to directories') + return Promise.reject() + } + + return result + }) + .then(stat => { + if (stat.err && /no such file or directory/.test(stat.err.message)) { + app.log.debug('Trails is creating the path (', pathName, ') at', dir) + } + mkdirp.sync(dir) + }) + })) + }, + + /** + * During config object inspection, we need to determine whether an arbitrary + * object is an external module loaded from a require statement. For example, + * the config object will contain trailpack modules, and we do not necessarily + * want to deep freeze all of them. In general, messing directly with loaded + * modules is not safe. + */ + getExternalModules () { + const rootPath = path.resolve(path.dirname(require.main.filename)) + const modulePath = path.join(rootPath, 'node_modules') + const requiredModules = Object.keys(require.cache).filter(mod => { + return mod.indexOf(modulePath) >= 0 + }) + return requiredModules + }, + + + /** + * Bind listeners various application events + */ + bindListeners (app) { + if (app.bound) { + app.log.warn('Someone attempted to bindListeners() twice! Stacktrace below.') + app.log.warn(console.trace()) // eslint-disable-line no-console + return + } + + Core.bindApplicationListeners(app) + app.bound = true + }, + + /** + * Bind listeners to trails application events + */ + bindApplicationListeners (app) { + app.once('trailpack:all:configured', () => { + if (app.config.main.freezeConfig !== true) { + app.log.warn('freezeConfig is disabled. Configuration will not be frozen.') + app.log.warn('Please only use this flag for testing/debugging purposes.') + } + + app.freezeConfig() + }) + app.once('trailpack:all:initialized', () => { + app.log.silly(app.config.motd.silly.initialized) + app.log.info(app.config.motd.info.initialized) + }) + app.once('trails:ready', () => { + app.log.info(app.config.motd.info.ready(app)) + app.log.debug(app.config.motd.debug.ready(app)) + app.log.silly(app.config.motd.silly.ready(app)) + + app.log.info(app.config.motd.hr) + }) + app.once('trails:stop', () => { + app.log.silly(app.config.motd.silly.stop) + app.log.info(app.config.motd.info.stop) + app.unfreezeConfig() + }) + app.once('trails:error:fatal', err => app.stop(err)) + }, + + /** + * Unbind all listeners that were bound during application startup + */ + unbindListeners (app) { + app.removeAllListeners() + app.bound = false + } +} diff --git a/lib/errors.js b/lib/errors.js index e8368e2..c25b07c 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -18,6 +18,10 @@ exports.ConfigNotDefinedError = class extends RangeError { - https://git.io/vw84F `) } + + static get name () { + return 'ConfigNotDefinedError' + } } exports.LoggerNotDefinedError = class extends RangeError { @@ -37,7 +41,10 @@ exports.LoggerNotDefinedError = class extends RangeError { For more info, see the config.log archetype: https://git.io/vVvUI `) - this.name = 'LoggerNotDefinedError' + } + + static get name () { + return 'LoggerNotDefinedError' } } @@ -68,15 +75,22 @@ exports.ApiNotDefinedError = class extends RangeError { - https://git.io/vw845 - https://git.io/vw84F `) - this.name = 'ApiNotDefinedError' + } + + static get name () { + return 'ApiNotDefinedError' } } exports.ConfigValueError = class extends RangeError { constructor(msg) { super(msg) - this.name = 'ConfigValueError' } + + static get name () { + return 'ConfigValueError' + } + } exports.PackageNotDefinedError = class extends RangeError { @@ -94,14 +108,20 @@ exports.PackageNotDefinedError = class extends RangeError { - https://git.io/vw845 - https://git.io/vw84F `) - this.name = 'PackageNotDefinedError' + } + + static get name () { + return 'PackageNotDefinedError' } } exports.IllegalAccessError = class extends Error { constructor(msg) { super(msg) - this.name = 'IllegalAccessError' + } + + static get name () { + return 'IllegalAccessError' } } @@ -110,8 +130,10 @@ exports.TimeoutError = class extends Error { super(` Timeout during "${phase}". Exceeded configured timeout of ${timeout}ms `) + } - this.name = 'TimeoutError' + static get name () { + return 'TimeoutError' } } @@ -137,6 +159,25 @@ exports.GraphCompletenessError = class extends RangeError { https://github.com/trailsjs/trails/issues. `) - this.name = 'GraphCompletenessError' + } + + static get name () { + return 'GraphCompletenessError' + } +} + +exports.NamespaceConflictError = class extends Error { + constructor (key, globals) { + super(` + The extant global variable "${key}" conflicts with the value provided by + Trails. + + Trails defines the following variables in the global namespace: + ${globals} + `) + } + + static get name () { + return 'NamespaceConflictError' } } diff --git a/lib/index.js b/lib/index.js index e3e97e2..f187041 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,6 +2,9 @@ exports.i18n = require('./i18n') exports.Trailpack = require('./trailpack') -exports.Trails = require('./trails') exports.Errors = require('./errors') exports.Pathfinder = require('./pathfinder') +exports.Core = require('./core') +exports.Schemas = require('./schemas') +exports.Configuration = require('./Configuration') +//exports.ConfigProxy = require('./ConfigProxy') diff --git a/lib/motd.js b/lib/motd.js new file mode 100644 index 0000000..1bad058 --- /dev/null +++ b/lib/motd.js @@ -0,0 +1,85 @@ +/*eslint max-len: 0 */ +const _ = require('lodash') + +module.exports = { + hr: '---------------------------------------------------------------', + + info: { + start: 'Starting...', + stop: 'Shutting down...', + initialized: 'All trailpacks are loaded.', + ready (app) { + const baseUrl = _.get(app.config, 'web.baseUrl') || + `http://${_.get(app.config, 'web.host') || 'localhost'}:${_.get(app.config, 'web.port') || '80'}` + return ( + `--------------------------------------------------------------- + ${new Date()} + Basic Info + Application : ${app.pkg.name} + Application root : ${baseUrl} + Version : ${app.pkg.version} + Environment : ${process.env.NODE_ENV}` + ) + } + }, + + debug: { + ready (app) { + return ( + ` Database Info + ORM : ${_.get(app.config, 'database.orm') || 'NOT INSTALLED'} + Stores : ${_.get(app.config, 'database.orm') ? Object.keys(app.config.database.stores) : 'N/A'} + Web Server Info + Server : ${_.get(app.config, 'web.server') || 'NOT INSTALLED'} + View Engine : ${_.get(app.config, 'views.engine') || 'NOT INSTALLED'} + Port : ${_.get(app.config, 'web.port') || 'N/A'} + Routes : ${(app.routes || [ ]).length}` + ) + } + }, + + silly: { + stop: ` + Happy trails to you, until we meet again. + - Dale Evans + `, + + ready (app) { + return ( + ` API + Models : ${_.keys(app.api.models)} + Controllers : ${_.keys(app.api.controllers)} + Policies : ${_.keys(app.api.policies)} + Trailpacks : ${_.map(app.packs, pack => pack.name)}` + ) + }, + + initialized: ` + ..@@@@.. .@. @@ + .@@@@@@@@@@@@@@@@. @@@ @@ + .@@@@' '@@@@. @@@ @@ + @@@@ @@@@ @@@ .. @@ + .@@; '@@. @@@ .@@@@. @@ + @@@ .@@@. .@@@@. @@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@ @@' '@@ @@ + @@' .@@@@@@@. .@@@@@@@@ '@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@ @@. .@@ @@ + @@' @@@@@@@@@@@ :@@@@@@@@@@ '@@ @@@ '@@@@' @@ + @@@ :@@@@@@@@@@@. @@@@@@@@@@@@ @@@ @@@ '' @@ + @@ @@@@@'@@@@@@@ :@@@@'@@@@@@@ @@ @@@ @@ .@@@@@@@@@@@. +.@@ @@@@@ @@@@@@@ :@@@@ @@@@@@@ @@: @@@ @@ .@@@@@@@ .@@@@@@@ @@ @@ .@@@@@@@@@@@@@@' +@@@ @@@' @@@@@@. @@@@ @@@@@@@ @@+ @@@ @@ .@@@@: .@@@@' @@ @@ @@@' +@@@ @@ @@@@@ '@' @@@@@' @@: @@@ @@@@@' @@@' @@ @@ .@@ +'@@ @@ @@ @@ @@@ @@@@ @@@ @@ @@ @@@ + @@. @@ @@ .@@ @@@ @@@ @@@ @@ @@ .@@. + @@. '@@@. @@ .@@ @@@ @@@ @@' .. @@ .@@@@...... + '@@ '@@@@@@@@@@.@@ .@@' @@@ @@' @@. .@@ @@ .@@@@@@@@@@@. + '@@. @@@@ .@@' @@@ @@' @@@ +@@ @@ @@ '@@@. + @@@. '@@ @@@ @@@ @@' '@@. .@@@ @@ @@ @@@ + '@@@. @@ .@@@' @@@ @@' '@@. .@@@@ @@ @@ @@@ + .@@@@. @@@@@@' @@@ @@' '@@@. .@@@ @@ @@ @@ @@@ + .@@@@@@@..... @@@. @@@ @@' '@@@@@.....@@@@@' @@ @@ @@ .@@@@@@@@@@@@@@@ + '@@@@@@@@ @@@ @@. '@@@@@@@@@' @@ @@ @@ '@@@@@@@@@@@@' + + ` + } +} + diff --git a/lib/schemas/api.js b/lib/schemas/api.js new file mode 100644 index 0000000..e9c51ef --- /dev/null +++ b/lib/schemas/api.js @@ -0,0 +1,9 @@ +const joi = require('joi') + +module.exports = joi.object().keys({ + controllers: joi.object().required(), + models: joi.object().required(), + policies: joi.object().required(), + services: joi.object().required(), + config: joi.object().optional() +}).unknown() diff --git a/lib/schemas/config.js b/lib/schemas/config.js new file mode 100644 index 0000000..30938f4 --- /dev/null +++ b/lib/schemas/config.js @@ -0,0 +1,18 @@ +const joi = require('joi') + +module.exports = joi.object().keys({ + main: joi.object().keys({ + packs: joi.array(), + paths: joi.object().keys({ + root: joi.string().required() + }).unknown(), + freezeConfig: joi.bool() + }).required().unknown(), + + env: joi.string().required(), + + log: joi.object().keys({ + logger: joi.object() + }).unknown() + +}).unknown() diff --git a/lib/schemas/index.js b/lib/schemas/index.js new file mode 100644 index 0000000..3a7bce4 --- /dev/null +++ b/lib/schemas/index.js @@ -0,0 +1,3 @@ +exports.api = require('./api') +exports.config = require('./config') +exports.package = require('./package') diff --git a/lib/schemas/package.js b/lib/schemas/package.js new file mode 100644 index 0000000..020e312 --- /dev/null +++ b/lib/schemas/package.js @@ -0,0 +1,7 @@ +const joi = require('joi') + +module.exports = joi.object().keys({ + dependencies: joi.object().keys({ + trails: joi.string() + }).unknown() +}).unknown() diff --git a/lib/trailpack.js b/lib/trailpack.js index 0dc08d7..d1d51ea 100644 --- a/lib/trailpack.js +++ b/lib/trailpack.js @@ -2,24 +2,6 @@ module.exports = { - /** - * Index trailpacks by name - */ - getTrailpackMapping (packs) { - return packs.reduce((mapping, pack) => { - mapping[pack.name] = pack - return mapping - }, { }) - }, - - /** - * Return all non-system trailpacks. As of v1.0, the only system trailpack is - * "trailpack-core" - */ - getUserlandTrailpacks (packs) { - return packs.filter(pack => pack.name !== 'core') - }, - /** * Bind lifecycle boundary event listeners. That is, when all trailpacks have * completed a particular phase, e.g. "configure" or "initialize", emit an @@ -31,6 +13,7 @@ module.exports = { const initializedEvents = packs.map(pack => `trailpack:${pack.name}:initialized`) app.after(configuredEvents) + .then(() => app.createPaths()) .then(() => app.emit('trailpack:all:configured')) .catch(err => app.stop(err)) @@ -55,19 +38,22 @@ module.exports = { const lifecycle = pack.config.lifecycle app.after(lifecycle.initialize.listen.concat('trailpack:all:configured')) + .then(() => app.log.debug('trailpack: initializing', pack.name)) .then(() => pack.initialize()) - .then(() => app.emit(`trailpack:${pack.name}:initialized`)) .catch(err => app.stop(err)) + .then(() => app.emit(`trailpack:${pack.name}:initialized`)) app.after(lifecycle.configure.listen.concat('trailpack:all:validated')) + .then(() => app.log.debug('trailpack: configuring', pack.name)) .then(() => pack.configure()) - .then(() => app.emit(`trailpack:${pack.name}:configured`)) .catch(err => app.stop(err)) + .then(() => app.emit(`trailpack:${pack.name}:configured`)) app.after('trails:start') + .then(() => app.log.debug('trailpack: validating', pack.name)) .then(() => pack.validate()) - .then(() => app.emit(`trailpack:${pack.name}:validated`)) .catch(err => app.stop(err)) + .then(() => app.emit(`trailpack:${pack.name}:validated`)) }) } } diff --git a/lib/trails.js b/lib/trails.js deleted file mode 100644 index f0e278b..0000000 --- a/lib/trails.js +++ /dev/null @@ -1,234 +0,0 @@ -/*eslint no-console: 0 */ -'use strict' - -const path = require('path') -const lib = require('.') - -module.exports = { - - /** - * Detect if the item is a Object. - */ - isObject (item) { - return (item && typeof item === 'object' && - !Array.isArray(item) && item !== null) - }, - - /** - * Deep Merge Objects - */ - deepMerge (target, source) { - if (this.isObject(target) && this.isObject(source)) { - Object.keys(source).forEach(key => { - if (this.isObject(source[key])) { - if (!target[key]) Object.assign(target, { [key]: {} }) - this.deepMerge(target[key], source[key]) - } - else { - Object.assign(target, { [key]: source[key] }) - } - }) - } - return target - }, - - /** - * Copy and merge the provided configuration into a new object, decorated with - * necessary default and environment-specific values. - */ - buildConfig (config) { - const configTemplate = { - main: { - maxListeners: 128, - packs: (config.main || { }).packs || [ ], - paths: { - root: path.resolve(path.dirname(require.main.filename)), - temp: path.resolve(path.dirname(require.main.filename), '.tmp') - }, - timeouts: { - start: 10000, - stop: 10000 - } - }, - log: { } - } - - // merge config.env - config = this.deepMerge( - (config || { }), - ((config.env && config.env[process.env.NODE_ENV]) || { }) - ) - - // merge config.main.paths - Object.assign( - configTemplate.main.paths, (config.main || { }).paths - ) - - // merge config.main.timeouts - Object.assign( - configTemplate.main.timeouts, (config.main || { }).timeouts - ) - - // merge application log - Object.assign(configTemplate.log, config.log) - - // merge remaining environment-specific config properties - return Object.assign({ }, config, configTemplate) - }, - - /** - * Validate the structure and prerequisites of the configuration object. Throw - * an Error if invalid; invalid configurations are unrecoverable and require - * that programmer fix them. - */ - validateConfig (config) { - if (!config || !config.main) { - throw new lib.Errors.ConfigNotDefinedError() - } - - if (!config.log || !config.log.logger) { - throw new lib.Errors.LoggerNotDefinedError() - } - - const nestedEnvs = lib.Trails.getNestedEnv(config) - if (nestedEnvs.length) { - throw new lib.Errors.ConfigValueError('Environment configs cannot contain an "env" property') - } - - if (config.env && config.env.env) { - throw new lib.Errors.ConfigValueError('config.env cannot contain an "env" property') - } - }, - - /** - * During config object inspection, we need to determine whether an arbitrary - * object is an external module loaded from a require statement. For example, - * the config object will contain trailpack modules, and we do not necessarily - * want to deep freeze all of them. In general, messing directly with loaded - * modules is not safe. - */ - getExternalModules () { - const rootPath = path.resolve(path.dirname(require.main.filename)) - const modulePath = path.join(rootPath, 'node_modules') - const requiredModules = Object.keys(require.cache).filter(mod => { - return mod.indexOf(modulePath) >= 0 - }) - return requiredModules - }, - - - /** - * Deep freeze application config object. Exceptions are made for required - * modules that are listed as dependencies in the application's - * package definition (package.json). - * - * @param config the configuration object to be frozen. - * @param [pkg] the package definition to use for exceptions. optional. - */ - freezeConfig (config, modules) { - const propNames = Object.getOwnPropertyNames(config) - - propNames.forEach(name => { - const prop = config[name] - - if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { - return - } - - const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) - if (ignoreModule) { - return - } - lib.Trails.freezeConfig(prop, modules) - }) - - Object.freeze(config) - }, - - /** - * Copy the configuration into a normal, unfrozen object - */ - unfreezeConfig (app, modules) { - const unfreeze = (target, source) => { - const propNames = Object.getOwnPropertyNames(source) - - propNames.forEach(name => { - const prop = source[name] - - if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { - target[name] = prop - return - } - - const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) - if (ignoreModule) { - return - } - - target[name] = { } - unfreeze(target[name], prop) - }) - - return target - } - - Object.defineProperties(app, { - config: { - value: unfreeze({ }, app.config), - configurable: true - } - }) - }, - - /** - * Check to see if the user defined a property config.env[env].env - */ - getNestedEnv (config) { - const env = (config && config.env) - const nestedEnvs = Object.keys(env || { }).filter(key => { - return !!env[key].env - }) - - return nestedEnvs - }, - - /** - * Bind listeners various application events - */ - bindEvents (app) { - if (app.bound) { - app.log.warn('Someone attempted to bindEvents() twice! Stacktrace below.') - app.log.warn(console.trace()) // eslint-disable-line no-console - return - } - - lib.Trails.bindApplicationEvents(app) - app.bound = true - }, - - /** - * Bind listeners to trails application events - */ - bindApplicationEvents (app) { - app.once('error', err => app.stop(err)) - app.once('trailpack:all:configured', () => { - if (app.config.main.freezeConfig !== false) { - app.log.warn('freezeConfig is disabled. Configuration will not be frozen.') - app.log.warn('Please only use this flag for testing/debugging purposes.') - lib.Trails.freezeConfig(app.config, app.loadedModules) - } - }) - app.once('trails:stop', () => { - lib.Trails.unfreezeConfig(app, app.loadedModules) - }) - app.once('trails:error:fatal', err => app.stop(err)) - }, - - /** - * Unbind all listeners that were bound during application startup - */ - unbindEvents (app) { - app.removeAllListeners() - app.bound = false - } -} diff --git a/model.js b/model.js new file mode 100644 index 0000000..d11ebf2 --- /dev/null +++ b/model.js @@ -0,0 +1 @@ +module.exports = require('trails-model') diff --git a/package.json b/package.json index 416943c..e45547a 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,8 @@ { "name": "trails", - "version": "1.1.1", + "version": "2.0.0", "description": "Modern Web Application Framework for Node.js", "keywords": [ - "mvc", "framework", "platform", "rest", @@ -13,20 +12,17 @@ "sails", "trails", "trailsjs", - "realtime", - "hapi", - "i18n", - "orm", - "server" + "server", + "graphql" ], "scripts": { - "test": "eslint . && mocha", + "test": "eslint --ignore-path .gitignore . && istanbul cover node_modules/mocha/bin/_mocha", "test-performance": "eslint . && mocha test-performance", - "coverage": "istanbul cover _mocha", + "coverage": "istanbul cover node_modules/mocha/bin/_mocha", "ci": "cd .. && ci" }, "pre-commit": [ - "test" + "test" ], "repository": { "type": "git", @@ -65,22 +61,41 @@ "url": "https://github.com/trailsjs/trails/issues" }, "homepage": "http://trailsjs.io", - "dependencies": {}, + "dependencies": { + "hoek": "4.1.0", + "i18next": "3.4.1", + "joi": "^10.0.1", + "lodash": "4.17.2", + "mkdirp": "0.5.1", + "trails-controller": "^2", + "trails-model": "^2", + "trails-policy": "^2", + "trails-service": "^2" + }, "devDependencies": { "eslint": "^2.5.3", - "eslint-config-trails": "^1.0", + "eslint-config-trails": "^2.0", "istanbul": "^0.4.2", "mocha": "^2.3.4", "pre-commit": "^1.1.3", - "smokesignals": "^1.2.0", - "trailpack": "^1.0.1", + "smokesignals": "v2-latest", + "trailpack": "v2-latest", "winston": "^2.1.1" }, + "bundledDependencies": [ + "trails-controller", + "trails-model", + "trails-policy", + "trails-service" + ], "eslintConfig": { "extends": "trails" }, "engines": { "node": ">= 4.0.0", "npm": ">= 2.14.2" + }, + "publishConfig": { + "tag": "next" } } diff --git a/policy.js b/policy.js new file mode 100644 index 0000000..856754e --- /dev/null +++ b/policy.js @@ -0,0 +1 @@ +module.exports = require('trails-policy') diff --git a/service.js b/service.js new file mode 100644 index 0000000..de13049 --- /dev/null +++ b/service.js @@ -0,0 +1 @@ +module.exports = require('trails-service') diff --git a/test/index.js b/test/index.js index 8f8c379..0cd0379 100644 --- a/test/index.js +++ b/test/index.js @@ -1,230 +1,6 @@ -'use strict' - -const assert = require('assert') -const smokesignals = require('smokesignals') const TrailsApp = require('..') -const testAppDefinition = require('./testapp') -const lib = require('../lib') - -describe('Trails', () => { - describe('@TrailsApp', () => { - describe('idempotence', () => { - it('should be able to start and stop many instances in a single node process', () => { - const cycles = [ ] - for (let i = 0; i < 10; ++i) { - cycles.push(new Promise (resolve => { - const app = new TrailsApp(testAppDefinition) - app.start(testAppDefinition) - .then(app => { - assert.equal(app.started, true) - resolve(app) - }) - })) - } - - return Promise.all(cycles) - .then(apps => { - return Promise.all(apps.map(app => { - return app.stop() - })) - }) - .then(apps => { - apps.map(app => { - assert.equal(app.stopped, true) - }) - }) - }) - it('should be able to stop, then start the same app', () => { - const app = new TrailsApp(testAppDefinition) - return app.start(testAppDefinition) - .then(app => { - assert.equal(app.started, true) - return app.stop() - }) - .then(app => { - assert.equal(app.stopped, true) - return app.start(testAppDefinition) - }) - .then(app => { - assert.equal(app.started, true) - return app.stop() - }) - }) - }) - describe('#constructor', () => { - let app - before(() => { - app = new TrailsApp(testAppDefinition) - }) - - describe('typical usage', () => { - it('should be instance of EventEmitter', () => { - assert(app instanceof require('events').EventEmitter) - }) - it('should set max number of event listeners', () => { - assert.equal(app.getMaxListeners(), 128) - }) - it('should set app properties', () => { - assert(app.pkg) - assert(app.config) - assert(app.api) - }) - it('should set NODE_ENV', () => { - assert.equal(process.env.NODE_ENV, 'development') - }) - }) - - describe('errors', () => { - describe('@LoggerNotDefinedError', () => { - it('should throw LoggerNotDefinedError if logger is missing', () => { - const def = { - pkg: { }, - api: { }, - config: { - main: { - paths: { root: __dirname } - } - } - } - assert.throws(() => new TrailsApp(def), lib.Errors.LoggerNotDefinedError) - }) - }) - describe('@ApiNotDefinedError', () => { - it('should throw ApiNotDefinedError if no api definition is provided', () => { - const def = { - pkg: { }, - config: { - main: { - paths: { root: __dirname } - }, - log: { - logger: new smokesignals.Logger('silent') - } - } - } - const app = new TrailsApp(def) - assert.throws(() => app.start(), lib.Errors.ApiNotDefinedError) - }) - }) - describe('@PackageNotDefinedError', () => { - it('should throw PackageNotDefinedError if no pkg definition is provided', () => { - const def = { - config: { - main: { - paths: { root: __dirname } - }, - log: { - logger: new smokesignals.Logger('silent') - } - } - } - assert.throws(() => new TrailsApp(def), lib.Errors.PackageNotDefinedError) - }) - }) - - it('should cache and freeze process.env', () => { - process.env.FOO = 'bar' - const def = { - api: { }, - config: { - main: { }, - log: { - logger: new smokesignals.Logger('silent') - } - }, - pkg: { } - } - const app = new TrailsApp(def) - - assert.equal(process.env.FOO, 'bar') - assert.equal(app.env.FOO, 'bar') - assert.throws(() => app.env.FOO = 1, TypeError) - }) - - it('should freeze config object after trailpacks are loaded', () => { - const def = { - pkg: { }, - api: { }, - config: { - main: { - packs: [ smokesignals.Trailpack ] - }, - log: { logger: new smokesignals.Logger('silent') }, - foo: 'bar' - } - } - const app = new TrailsApp(def) - assert.equal(app.config.foo, 'bar') - - app.start() - return app.after('trailpack:all:configured').then(() => { - assert.equal(app.config.foo, 'bar') - assert.throws(() => app.config.foo = 1, TypeError) - return app.stop() - }) - }) - - it('should disallow re-assignment of config object', () => { - const def = { - pkg: { }, - api: { }, - config: { - main: { - packs: [ smokesignals.Trailpack ] - }, - log: { logger: new smokesignals.Logger('silent') }, - foo: 'bar' - } - } - const app = new TrailsApp(def) - assert.equal(app.config.foo, 'bar') - assert.throws(() => app.config = { }, Error) - }) - }) - }) - - describe('#after', () => { - let app - before(() => { - app = new TrailsApp(testAppDefinition) - }) - - it('should invoke listener when listening for a single event', () => { - const eventPromise = app.after([ 'test1' ]) - app.emit('test1') - return eventPromise - }) - it('should accept a single event as an array or a string', () => { - const eventPromise = app.after('test1') - app.emit('test1') - return eventPromise - }) - it('should invoke listener when listening for multiple events', () => { - const eventPromise = app.after([ 'test1', 'test2', 'test3' ]) - app.emit('test1') - app.emit('test2') - app.emit('test3') - - return eventPromise - }) - it('should invoke listener when listening for multiple possible events', () => { - const eventPromise = app.after([['test1', 'test2'], 'test3']) - app.emit('test1') - app.emit('test3') - - return eventPromise - }) - it('should pass event parameters to callbacks added using `onceAny`', done => { - const sent = { test: true } - - app.onceAny('test', received => { - assert.equal(received, sent) - - return done() - }) - app.emit('test', sent) - }) - }) - }) +before(() => { + global.app = new TrailsApp(require('./integration/app')) + return global.app.start() }) diff --git a/test/integration/app.js b/test/integration/app.js new file mode 100644 index 0000000..dc11c4d --- /dev/null +++ b/test/integration/app.js @@ -0,0 +1,53 @@ +const path = require('path') +const _ = require('lodash') +const smokesignals = require('smokesignals') +const Testpack = require('./testpack') + +const AppConfigLocales = { + en: { + helloworld: 'hello world', + hello: { + user: 'hello {{username}}' + } + }, + de: { + helloworld: 'hallo Welt', + hello: { + user: 'hallo {{username}}' + } + } +} + +const App = { + pkg: { + name: 'core-trailpack-test' + }, + api: { + customkey: {} + }, + config: { + main: { + packs: [ + Testpack + ], + paths: { + testdir: path.resolve(__dirname, 'testdir') + } + }, + i18n: { + lng: 'en', + resources: { + en: { + translation: AppConfigLocales.en + }, + de: { + translation: AppConfigLocales.de + } + } + } + }, + locales: AppConfigLocales +} + +_.defaultsDeep(App, smokesignals.FailsafeConfig) +module.exports = App diff --git a/test/integration/i18n.test.js b/test/integration/i18n.test.js new file mode 100644 index 0000000..12f054b --- /dev/null +++ b/test/integration/i18n.test.js @@ -0,0 +1,34 @@ +const assert = require('assert') +const _ = require('lodash') + +describe('i18n', () => { + it('should expose the __ method on the app object', () => { + assert(global.app.__) + assert(_.isFunction(global.app.__)) + }) + describe('#__', () => { + describe('EN (default)', () => { + it('should render EN strings by default', () => { + assert.equal(global.app.__('helloworld'), 'hello world') + }) + it('should render nested string', () => { + assert.equal( + global.app.__('hello.user', { username: 'trails' }), + 'hello trails' + ) + }) + }) + describe('DE', () => { + it('should render DE string when specified', () => { + assert.equal(global.app.__('helloworld', { lng: 'de' }), 'hallo Welt') + }) + it('should render nested string', () => { + assert.equal( + global.app.__('hello.user', { lng: 'de', username: 'trails' }), + 'hallo trails' + ) + }) + }) + }) +}) + diff --git a/test/integration/index.js b/test/integration/index.js new file mode 100644 index 0000000..e77d960 --- /dev/null +++ b/test/integration/index.js @@ -0,0 +1,285 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const assert = require('assert') +const smokesignals = require('smokesignals') +const TrailsApp = require('../..') +const Testpack = require('./testpack') +const testAppDefinition = require('./testapp') +const lib = require('../../lib') + +describe('Trails', () => { + describe('@TrailsApp', () => { + describe('idempotence', () => { + it('should be able to start and stop many instances in a single node process', () => { + const cycles = [ ] + for (let i = 0; i < 10; ++i) { + cycles.push(new Promise (resolve => { + const app = new TrailsApp(testAppDefinition) + app.start(testAppDefinition) + .then(app => { + assert.equal(app.started, true) + resolve(app) + }) + })) + } + + return Promise.all(cycles) + .then(apps => { + return Promise.all(apps.map(app => { + return app.stop() + })) + }) + .then(apps => { + apps.map(app => { + assert.equal(app.stopped, true) + }) + }) + }) + it('should be able to stop, then start the same app', () => { + const app = new TrailsApp(testAppDefinition) + return app.start(testAppDefinition) + .then(app => { + assert.equal(app.started, true) + return app.stop() + }) + .then(app => { + assert.equal(app.stopped, true) + return app.start(testAppDefinition) + }) + .then(app => { + assert.equal(app.started, true) + return app.stop() + }) + }) + }) + describe('#constructor', () => { + let app + before(() => { + app = new TrailsApp(testAppDefinition) + }) + + describe('typical usage', () => { + it('should be instance of EventEmitter', () => { + assert(app instanceof require('events').EventEmitter) + }) + it('should set max number of event listeners', () => { + assert.equal(app.getMaxListeners(), 128) + }) + it('should set app properties', () => { + assert(app.pkg) + assert(app.config) + assert(app.api) + }) + it('should set NODE_ENV', () => { + assert.equal(process.env.NODE_ENV, 'development') + }) + }) + + describe('configuration', () => { + it('should validate an api object with arbitrary keys', () => { + assert(global.app.api.customkey) + }) + it('should create missing directories for configured paths', () => { + assert(fs.statSync(path.resolve(__dirname, 'testdir'))) + }) + it('should set paths.temp if not configured explicitly by user', () => { + assert(global.app.config.main.paths.temp) + }) + it('should set paths.logs if not configured explicitly by user', () => { + assert(global.app.config.main.paths.logs) + }) + it('should set paths.sockets if not configured explicitly by user', () => { + assert(global.app.config.main.paths.sockets) + }) + }) + + describe('errors', () => { + describe('@LoggerNotDefinedError', () => { + it('should throw LoggerNotDefinedError if logger is missing', () => { + const def = { + pkg: { }, + api: { }, + config: { + main: { + paths: { root: __dirname } + } + } + } + assert.throws(() => new TrailsApp(def), lib.Errors.LoggerNotDefinedError) + }) + }) + describe('@ApiNotDefinedError', () => { + it('should throw ApiNotDefinedError if no api definition is provided', () => { + const def = { + pkg: { }, + config: { + main: { + paths: { root: __dirname } + }, + log: { + logger: new smokesignals.Logger('silent') + } + } + } + assert.throws(() => new TrailsApp(def), lib.Errors.ApiNotDefinedError) + }) + }) + describe('@PackageNotDefinedError', () => { + it('should throw PackageNotDefinedError if no pkg definition is provided', () => { + const def = { + config: { + main: { + paths: { root: __dirname } + }, + log: { + logger: new smokesignals.Logger('silent') + } + } + } + assert.throws(() => new TrailsApp(def), lib.Errors.PackageNotDefinedError) + }) + }) + + it('should cache and freeze process.env', () => { + process.env.FOO = 'bar' + const def = { + api: { }, + config: { + main: { }, + log: { + logger: new smokesignals.Logger('silent') + } + }, + pkg: { } + } + const app = new TrailsApp(def) + + assert.equal(process.env.FOO, 'bar') + assert.equal(app.env.FOO, 'bar') + assert.throws(() => app.env.FOO = 1, TypeError) + }) + + it('should freeze config object after trailpacks are loaded', () => { + const def = { + pkg: { }, + api: { }, + config: { + main: { + packs: [ Testpack ] + }, + log: { logger: new smokesignals.Logger('silent') }, + foo: 'bar' + } + } + const app = new TrailsApp(def) + assert.equal(app.config.foo, 'bar') + + app.start() + return app.after('trailpack:all:configured').then(() => { + assert.equal(app.config.foo, 'bar') + assert.throws(() => app.config.foo = 1, TypeError) + return app.stop() + }) + }) + + it('should disallow re-assignment of config object', () => { + const def = { + pkg: { }, + api: { }, + config: { + main: { + packs: [ Testpack ] + }, + log: { logger: new smokesignals.Logger('silent') }, + foo: 'bar' + } + } + const app = new TrailsApp(def) + assert.equal(app.config.foo, 'bar') + assert.throws(() => app.config = { }, Error) + }) + }) + }) + + describe('#after', () => { + let app + before(() => { + app = new TrailsApp(testAppDefinition) + }) + + it('should invoke listener when listening for a single event', () => { + const eventPromise = app.after([ 'test1' ]) + app.emit('test1') + return eventPromise + }) + it('should accept a single event as an array or a string', () => { + const eventPromise = app.after('test2') + app.emit('test2') + return eventPromise + }) + it('should invoke listener when listening for multiple events', () => { + const eventPromise = app.after([ 'test3', 'test4', 'test5' ]) + app.emit('test3') + app.emit('test4') + app.emit('test5') + + return eventPromise + }) + it('should invoke listener when listening for multiple possible events', () => { + const eventPromise = app.after([['test6', 'test7'], 'test8']) + app.emit('test6') + app.emit('test8') + + return eventPromise + }) + it('should pass event parameters through to handler', () => { + const eventPromise = app.after(['test9', 'test10']) + .then(results => { + assert.equal(results[0], 9) + assert.equal(results[1], 10) + }) + + app.emit('test9', 9) + app.emit('test10', 10) + + return eventPromise + }) + it('should accept a callback as the 2nd argument to invoke instead of returning a Promise', done => { + app.after(['test11', 'test12'], results => { + assert.equal(results[0], 11) + assert.equal(results[1], 12) + done() + }) + app.emit('test11', 11) + app.emit('test12', 12) + }) + }) + + describe('#onceAny', () => { + let app + before(() => { + app = new TrailsApp(testAppDefinition) + }) + + it('should pass event parameters through to handler', () => { + const eventPromise = app.onceAny('test1') + .then(result => { + assert.equal(result[0], 1) + }) + + app.emit('test1', 1) + + return eventPromise + }) + it('should accept a callback as the 2nd argument to invoke instead of returning a Promise', done => { + app.onceAny(['test1', 'test2'], t1 => { + assert.equal(t1, 1) + done() + }) + app.emit('test1', 1) + }) + }) + }) +}) diff --git a/test/testapp.js b/test/integration/testapp.js similarity index 72% rename from test/testapp.js rename to test/integration/testapp.js index 513a03b..c81919c 100644 --- a/test/testapp.js +++ b/test/integration/testapp.js @@ -1,4 +1,5 @@ const smokesignals = require('smokesignals') +const Testpack = require('./testpack') module.exports = { api: { @@ -8,7 +9,10 @@ module.exports = { main: { paths: { root: __dirname - } + }, + packs: [ + Testpack + ] }, log: { logger: new smokesignals.Logger('silent') diff --git a/test/integration/testpack.js b/test/integration/testpack.js new file mode 100644 index 0000000..4b5d088 --- /dev/null +++ b/test/integration/testpack.js @@ -0,0 +1,13 @@ +'use strict' + +const Trailpack = require('trailpack') + +module.exports = class Testpack extends Trailpack { + constructor (app) { + super(app, { + pkg: { + name: 'testpack' + } + }) + } +} diff --git a/test/lib/trails.test.js b/test/lib/Configuration.test.js similarity index 59% rename from test/lib/trails.test.js rename to test/lib/Configuration.test.js index 7c6cc8e..9cf862a 100644 --- a/test/lib/trails.test.js +++ b/test/lib/Configuration.test.js @@ -1,92 +1,92 @@ 'use strict' -const path = require('path') const assert = require('assert') -const smokesignals = require('smokesignals') const lib = require('../../lib') - -describe('lib.Trails', () => { - - describe('#buildConfig', () => { - const NODE_ENV = process.env.NODE_ENV - let testConfig - - beforeEach(() => { - testConfig = { - env: { - envTest1: { - log: { +const smokesignals = require('smokesignals') +const _ = require('lodash') + +describe('lib.Configuration', () => { + const NODE_ENV = process.env.NODE_ENV + let testConfig + + beforeEach(() => { + testConfig = { + env: { + envTest1: { + log: { + merged: 'yes', + extraneous: 'assigned' + }, + customObject: { + string: 'b', + int: 2, + array: [2, 3, 4], + subobj: { + attr: 'b' + } + } + }, + envTest2: { + log: { + nested: { merged: 'yes', extraneous: 'assigned' }, - customObject: { - string: 'b', - int: 2, - array: [2, 3, 4], - subobj: { - attr: 'b' - } - } + merged: 'yes', + extraneous: 'assigned' }, - envTest2: { - log: { - nested: { + customObject: { + subobj: { + attr: 'b' + }, + int2: 2 + } + }, + envTest3: { + log: { + nested: { + merged: 'yes', + extraneous: 'assigned', + deeplyNested: { merged: 'yes', extraneous: 'assigned' - }, - merged: 'yes', - extraneous: 'assigned' + } }, - customObject: { - subobj: { - attr: 'b' - }, - int2: 2 - } - }, - envTest3: { - log: { - nested: { - merged: 'yes', - extraneous: 'assigned', - deeplyNested: { - merged: 'yes', - extraneous: 'assigned' - } - }, - merged: 'yes', - extraneous: 'assigned' - } + merged: 'yes', + extraneous: 'assigned' } - }, - log: { + } + }, + log: { + logger: new smokesignals.Logger('silent'), + merged: 'no', + nested: { merged: 'no', - nested: { - merged: 'no', - deeplyNested: { - merged: 'no' - } - }, - normal: 'yes' - }, - customObject: { - string: 'a', - int: 1, - array: [1, 2, 3], - subobj: { - attr: 'a' + deeplyNested: { + merged: 'no' } + }, + normal: 'yes' + }, + customObject: { + string: 'a', + int: 1, + array: [1, 2, 3], + subobj: { + attr: 'a' } } - }) + } + }) - afterEach(() => { - process.env.NODE_ENV = NODE_ENV - }) + afterEach(() => { + process.env.NODE_ENV = NODE_ENV + }) + + describe('#buildConfig', () => { it('should merge basic env config', () => { - process.env.NODE_ENV = 'envTest1' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Configuration.buildConfig(testConfig, 'envTest1') assert(config) assert.equal(config.log.merged, 'yes') @@ -99,7 +99,7 @@ describe('lib.Trails', () => { it('should merge nested env config', () => { process.env.NODE_ENV = 'envTest2' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Configuration.buildConfig(testConfig, 'envTest2') assert(config) assert.equal(config.log.merged, 'yes') @@ -116,7 +116,7 @@ describe('lib.Trails', () => { it('should merge deeply nested env config', () => { process.env.NODE_ENV = 'envTest3' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Configuration.buildConfig(testConfig, 'envTest3') assert(config) assert.equal(config.log.merged, 'yes') @@ -136,7 +136,7 @@ describe('lib.Trails', () => { it('should merge full custom env config', () => { process.env.NODE_ENV = 'envTest1' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Configuration.buildConfig(testConfig, 'envTest1') assert(config) assert(typeof config.customObject === 'object') @@ -150,7 +150,7 @@ describe('lib.Trails', () => { it('should merge partial custom env config', () => { process.env.NODE_ENV = 'envTest2' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Configuration.buildConfig(testConfig, 'envTest2') assert(config) assert(typeof config.customObject === 'object') @@ -164,7 +164,7 @@ describe('lib.Trails', () => { it('should merge new custom attr in env config', () => { process.env.NODE_ENV = 'envTest2' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Configuration.buildConfig(testConfig, 'envTest2') assert(config) assert(typeof config.customObject === 'object') @@ -179,12 +179,11 @@ describe('lib.Trails', () => { it('should not override any configs if NODE_ENV matches no env', () => { process.env.NODE_ENV = 'notconfigured' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Configuration.buildConfig(testConfig, 'notconfigured') assert(config) assert.equal(config.log.merged, 'no') assert.equal(config.log.normal, 'yes') - assert.equal(config.customObject, testConfig.customObject) assert(!config.log.extraneous) assert(config.env) @@ -193,13 +192,12 @@ describe('lib.Trails', () => { it('should keep "env" property from config', () => { process.env.NODE_ENV = 'mergetest2' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Configuration.buildConfig(testConfig, 'mergetest2') assert(config.env) }) }) - - describe('#getNestedEnv', () => { - it('should return a list of envs if one contains a "env" property', () => { + describe('#validateConfig', () => { + it('should throw ConfigValueError if an env config contains the "env" property', () => { const testConfig = { main: { }, log: { @@ -207,56 +205,56 @@ describe('lib.Trails', () => { }, env: { envtest: { - env: { - invalid: true - } + env: 'hello' } } } - - const nestedEnvs = lib.Trails.getNestedEnv(testConfig) - - assert.equal(nestedEnvs[0], 'envtest') - assert.equal(nestedEnvs.length, 1) + assert.throws(() => lib.Configuration.validateConfig(testConfig), lib.Errors.ConfigValueError) + assert.throws(() => lib.Configuration.validateConfig(testConfig), /Environment configs/) }) - }) - - describe('#validateConfig', () => { - it('should throw ConfigValueError if an env config contains the "env" property', () => { + it('should throw ConfigValueError if config.env contains the "env" property', () => { const testConfig = { main: { }, log: { logger: new smokesignals.Logger('silent') }, env: { - envtest: { - env: 'hello' - } + env: 'hello' } } - assert.throws(() => lib.Trails.validateConfig(testConfig), lib.Errors.ConfigValueError) - assert.throws(() => lib.Trails.validateConfig(testConfig), /Environment configs/) + assert.throws(() => lib.Configuration.validateConfig(testConfig), lib.Errors.ConfigValueError) + assert.throws(() => lib.Configuration.validateConfig(testConfig), /config.env/) }) - it('should throw ConfigValueError if config.env contains the "env" property', () => { + }) + describe('#getNestedEnv', () => { + it('should return a list of envs if one contains a "env" property', () => { const testConfig = { main: { }, log: { logger: new smokesignals.Logger('silent') }, env: { - env: 'hello' + envtest: { + env: { + invalid: true + } + } } } - assert.throws(() => lib.Trails.validateConfig(testConfig), lib.Errors.ConfigValueError) - assert.throws(() => lib.Trails.validateConfig(testConfig), /config.env/) + + const nestedEnvs = lib.Configuration.getNestedEnv(testConfig) + + assert.equal(nestedEnvs[0], 'envtest') + assert.equal(nestedEnvs.length, 1) }) }) - describe('#freezeConfig', () => { it('should freeze nested object', () => { const o1 = { foo: { bar: 1 } } - lib.Trails.freezeConfig(o1, [ ]) + lib.Configuration.freezeConfig(o1, [ ]) + assert(Object.isFrozen(o1)) + assert(Object.isFrozen(o1.foo)) assert.throws(() => o1.foo = null, Error) }) it('should not freeze exernal modules required from config', () => { @@ -264,7 +262,7 @@ describe('lib.Trails', () => { foo: require('smokesignals'), bar: 1 } - lib.Trails.freezeConfig(o1, [ require.resolve('smokesignals') ]) + lib.Configuration.freezeConfig(o1, [ require.resolve('smokesignals') ]) assert.throws(() => o1.bar = null, Error) @@ -273,27 +271,32 @@ describe('lib.Trails', () => { }) // https://bugs.chromium.org/p/v8/issues/detail?id=4460 - if (!/^v6/.test(process.version)) { - it('v8 issue 4460 exists', () => { + if (/^(v4)|(v5)/.test(process.version)) { + it('v8 issue 4460 exists in node v4, v5 series (cannot naively freeze Int8Aray)', () => { assert.throws(() => Object.freeze(new Int8Array()), TypeError) //assert.throws(() => Object.freeze(new Buffer([1,2,3])), TypeError) //assert.throws(() => Object.freeze(new DataView()), TypeError) }) } + else { + it('v8 issue 4460 is resolved (node 6 and newer)', () => { + assert(true) + }) + } + it('should freeze objects containing unfreezable types without error', () => { const o1 = { typedArray: new Int8Array(), buffer: new Buffer([ 1,2,3 ]), fun: function () { } } - lib.Trails.freezeConfig(o1, [ ]) + lib.Configuration.freezeConfig(o1, [ ]) assert(o1.typedArray) assert(Buffer.isBuffer(o1.buffer)) assert(o1.fun) }) }) - describe('#unfreezeConfig', () => { it('should unfreeze shallow config object', () => { const app = { @@ -302,10 +305,10 @@ describe('lib.Trails', () => { foo: 'bar' } } - lib.Trails.freezeConfig(app.config, [ ]) + lib.Configuration.freezeConfig(app.config, [ ]) assert.throws(() => app.config.a = 2, Error) - lib.Trails.unfreezeConfig(app, [ ]) + app.config = lib.Configuration.unfreezeConfig(app.config, [ ]) app.config.a = 2 assert.equal(app.config.a, 2) }) @@ -321,31 +324,54 @@ describe('lib.Trails', () => { } } } - lib.Trails.freezeConfig(app.config, [ ]) + lib.Configuration.freezeConfig(app.config, [ ]) assert.throws(() => app.config.main.paths.root = 'newrootpath', Error) - lib.Trails.unfreezeConfig(app, [ ]) + app.config = lib.Configuration.unfreezeConfig(app.config, [ ]) app.config.main.paths.root = 'newrootpath' assert.equal(app.config.main.paths.root, 'newrootpath') assert.equal(app.config.main.paths.temp, 'temppath') assert.equal(app.config.main.foo, 1) }) }) - - describe('#getExternalModules', () => { - const rmf = require.main.filename - - beforeEach(() => { - require.main.filename = path.resolve(__dirname, '..', '..', 'index.js') + describe('#get', () => { + it('should return nested config value if it exists', () => { + const config = new lib.Configuration(testConfig, { NODE_ENV: 'test' }) + assert.equal(config.get('customObject.string'), 'a') }) - afterEach(() => { - require.main.filename = rmf + it('should return undefined if config value does not exist', () => { + const config = new lib.Configuration(testConfig, { NODE_ENV: 'test' }) + assert.equal(config.get('customObject.nobody'), undefined) }) - it('should return external modules', () => { - const modules = lib.Trails.getExternalModules() - assert(modules.indexOf(require.resolve('mocha')) !== -1) + it('should return undefined if any config tree path segment does not exist', () => { + const config = new lib.Configuration(testConfig, { NODE_ENV: 'test' }) + assert.equal(config.get('i.dont.exist'), undefined) + }) + it('should return the nested config object if a path is given to an internal node', () => { + const config = new lib.Configuration(testConfig, { NODE_ENV: 'test' }) + assert.equal(config.get('customObject').string, 'a') }) }) -}) + describe('#set', () => { + it('should set the value of a leaf node', () => { + const config = new lib.Configuration(_.cloneDeep(testConfig), { NODE_ENV: 'test' }) + config.set('customObject.testValue', 'test') + assert.equal(config.get('customObject.testValue'), 'test') + assert.equal(config.customObject.testValue, 'test') + }) + it('should set the value of a new, nested leaf node with no pre-existing path', () => { + const config = new lib.Configuration(_.cloneDeep(testConfig), { NODE_ENV: 'test' }) + + assert(!config.foo) + config.set('foo.bar.new.path', 'test') + + assert.equal(config.get('foo.bar.new.path'), 'test') + assert.equal(config.foo.bar.new.path, 'test') + assert(_.isPlainObject(config.foo)) + assert(_.isPlainObject(config.foo.bar)) + assert(_.isPlainObject(config.foo.bar.new)) + }) + }) +}) diff --git a/test/lib/Core.test.js b/test/lib/Core.test.js new file mode 100644 index 0000000..5f69b7a --- /dev/null +++ b/test/lib/Core.test.js @@ -0,0 +1,86 @@ +'use strict' + +const path = require('path') +const assert = require('assert') +const _ = require('lodash') +const lib = require('../../lib') + +describe('lib.Core', () => { + describe('#getClassMethods', () => { + const A = class A { + foo () { } + } + const B = class B extends A { + bar () { } + } + const C = class B extends A { + bar () { } + baz () { } + get getter () { + return 'getter' + } + static staticThing () { } + } + it('should return class methods for object', () => { + const methods = lib.Core.getClassMethods(new A(), A) + + assert.equal(methods.length, 1) + assert.equal(methods[0], 'foo') + }) + it('should return class methods for all objects in prototype chain', () => { + const methods = lib.Core.getClassMethods(new B(), B) + + assert.equal(methods.length, 2) + assert(_.includes(methods, 'foo')) + assert(_.includes(methods, 'bar')) + }) + it('should return only *instance methods* and no other type of thing', () => { + const methods = lib.Core.getClassMethods(new C(), C) + + assert.equal(methods.length, 3) + assert(_.includes(methods, 'bar')) + assert(_.includes(methods, 'foo')) + assert(_.includes(methods, 'baz')) + }) + }) + + describe('#getExternalModules', () => { + const rmf = require.main.filename + + beforeEach(() => { + require.main.filename = path.resolve(__dirname, '..', '..', 'index.js') + }) + afterEach(() => { + require.main.filename = rmf + }) + it('should return external modules', () => { + const modules = lib.Core.getExternalModules() + assert.notEqual(modules.indexOf(require.resolve('trailpack')), -1) + }) + }) + + describe('#assignGlobals', () => { + it('should assign variables to the global namespace', () => { + lib.Core.assignGlobals() + + assert(global.Service) + assert(Service) + }) + it('global variables should be immutable and error if mutation is attempted', () => { + assert.throws(() => delete global.Service, Error) + assert(global.Service) + assert(Service) + }) + it('should ignore conflicts for identical values', () => { + const s1 = Service + lib.Core.assignGlobals() + lib.Core.assignGlobals() + lib.Core.assignGlobals() + lib.Core.assignGlobals() + + assert(global.Service) + assert(Service) + assert.equal(s1, Service) + }) + }) +}) diff --git a/test/lib/pathfinder.test.js b/test/lib/pathfinder.test.js index aecd94f..dda3b14 100644 --- a/test/lib/pathfinder.test.js +++ b/test/lib/pathfinder.test.js @@ -1,8 +1,9 @@ 'use strict' +const EventEmitter = require('events').EventEmitter const assert = require('assert') const lib = require('../../lib') -const smokesignals = require('smokesignals') +const Trailpack = require('trailpack') describe('lib.Pathfinder', () => { describe('#getPathErrors', () => { @@ -121,27 +122,38 @@ describe('lib.Pathfinder', () => { }) }) describe('#getEventProducer', () => { + const app = new EventEmitter() const packs = [ - new smokesignals.Trailpack(new smokesignals.TrailsApp(), { - trailpack: { - lifecycle: { - configure: { - emit: [ 'pack1:configured', 'pack1:custom' ] - }, - initialize: { - emit: [ 'pack1:initialized', 'pack1:custom' ] + new Trailpack(app, { + pkg: { + name: 'pack1' + }, + config: { + trailpack: { + lifecycle: { + configure: { + emit: [ 'pack1:configured', 'pack1:custom' ] + }, + initialize: { + emit: [ 'pack1:initialized', 'pack1:custom' ] + } } } } }), - new smokesignals.Trailpack(new smokesignals.TrailsApp(), { - trailpack: { - lifecycle: { - configure: { - emit: [ 'pack2:configured' ] - }, - initialize: { - emit: [ 'pack2:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack2' + }, + config: { + trailpack: { + lifecycle: { + configure: { + emit: [ 'pack2:configured' ] + }, + initialize: { + emit: [ 'pack2:initialized' ] + } } } } @@ -162,98 +174,128 @@ describe('lib.Pathfinder', () => { }) describe('Lifecycle', () => { - const app = new smokesignals.TrailsApp() + const app = new EventEmitter() const packs = [ - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - configure: { - listen: [ ], - emit: [ 'pack0:configured' ] - }, - initialize: { - listen: [ ], - emit: [ 'pack0:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack0' + }, + config: { + trailpack: { + lifecycle: { + configure: { + listen: [ ], + emit: [ 'pack0:configured' ] + }, + initialize: { + listen: [ ], + emit: [ 'pack0:initialized' ] + } } } } - }, 'pack0'), + }), - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - configure: { - listen: [ 'pack0:configured' ], - emit: [ 'pack1:configured' ] - }, - initialize: { - emit: [ 'pack1:initialized', 'pack1:custom' ] + new Trailpack(app, { + pkg: { + name: 'pack1' + }, + config: { + trailpack: { + lifecycle: { + configure: { + listen: [ 'pack0:configured' ], + emit: [ 'pack1:configured' ] + }, + initialize: { + emit: [ 'pack1:initialized', 'pack1:custom' ] + } } } } - }, 'pack1'), + }), - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - configure: { - listen: [ 'pack1:configured' ], - emit: [ 'pack2:configured' ] - }, - initialize: { - listen: [ 'pack1:initialized', 'pack1:custom' ], - emit: [ 'pack2:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack2' + }, + config: { + trailpack: { + lifecycle: { + configure: { + listen: [ 'pack1:configured' ], + emit: [ 'pack2:configured' ] + }, + initialize: { + listen: [ 'pack1:initialized', 'pack1:custom' ], + emit: [ 'pack2:initialized' ] + } } } } - }, 'pack2'), + }), - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - configure: { - listen: [ 'pack2:configured' ], - emit: [ 'pack3:configured' ] - }, - initialize: { - listen: [ 'pack2:initialized', 'pack1:custom' ], - emit: [ 'pack3:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack3' + }, + config: { + trailpack: { + lifecycle: { + configure: { + listen: [ 'pack2:configured' ], + emit: [ 'pack3:configured' ] + }, + initialize: { + listen: [ 'pack2:initialized', 'pack1:custom' ], + emit: [ 'pack3:initialized' ] + } } } } - }, 'pack3'), + }), - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - // dependency with no route to source - configure: { - listen: [ 'packX:configured' ], - emit: [ 'pack4:configured' ] - }, - // dependency on pack with circular dependency - initialize: { - listen: [ 'pack5:initialized', 'pack0:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack4' + }, + config: { + trailpack: { + lifecycle: { + // dependency with no route to source + configure: { + listen: [ 'packX:configured' ], + emit: [ 'pack4:configured' ] + }, + // dependency on pack with circular dependency + initialize: { + listen: [ 'pack5:initialized', 'pack0:initialized' ] + } } } } - }, 'pack4'), + }), // circular dependency - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - configure: { - listen: [ 'pack5:configured' ], - emit: [ 'pack5:configured' ] - }, - initialize: { - listen: [ 'pack4:initialized' ], - emit: [ 'pack5:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack5' + }, + config: { + trailpack: { + lifecycle: { + configure: { + listen: [ 'pack5:configured' ], + emit: [ 'pack5:configured' ] + }, + initialize: { + listen: [ 'pack4:initialized' ], + emit: [ 'pack5:initialized' ] + } } } } - }, 'pack5') + }) ] describe('#getLifecyclePath', () => { diff --git a/test/lib/trailpack.test.js b/test/lib/trailpack.test.js deleted file mode 100644 index 56f527c..0000000 --- a/test/lib/trailpack.test.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict' - -const assert = require('assert') -const lib = require('../../lib') -const Trailpack = require('trailpack') -const testAppDefinition = require('../testapp') -const TrailsApp = require('../../') - -describe('lib.Trailpack', () => { - const app = new TrailsApp(testAppDefinition) - - before(() => { - return app.start(testAppDefinition) - }) - - describe('#getUserlandTrailpacks', () => { - let testTrailpacks - before(() => { - testTrailpacks = [ - new Trailpack(app, { pkg: { name: 'trailpack-pack1' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack2' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack3' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack4' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-core' }, config: { } }) - ] - }) - - it('should exclude the core trailpack', () => { - const packs = lib.Trailpack.getUserlandTrailpacks(testTrailpacks) - - assert(packs) - assert.equal(packs.length, 4) - }) - }) - describe('#getTrailpackMapping', () => { - let testTrailpacks - before(() => { - testTrailpacks = [ - new Trailpack(app, { pkg: { name: 'trailpack-pack1' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack2' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack3' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack4' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-core' }, config: { } }) - ] - }) - - it('should index packs by name', () => { - const packs = lib.Trailpack.getTrailpackMapping(testTrailpacks) - - assert(packs.pack1) - assert(packs.pack2) - assert(packs.pack3) - assert(packs.pack4) - assert(packs.core) - }) - - }) -}) -