index.js
1 'use strict'; 2 3 var util = require('util'); 4 var define = require('define-property'); 5 var CacheBase = require('cache-base'); 6 var Emitter = require('component-emitter'); 7 var isObject = require('isobject'); 8 var merge = require('mixin-deep'); 9 var pascal = require('pascalcase'); 10 var cu = require('class-utils'); 11 12 /** 13 * Optionally define a custom `cache` namespace to use. 14 */ 15 16 function namespace(name) { 17 var Cache = name ? CacheBase.namespace(name) : CacheBase; 18 var fns = []; 19 20 /** 21 * Create an instance of `Base` with the given `config` and `options`. 22 * 23 * ```js 24 * // initialize with `config` and `options` 25 * var app = new Base({isApp: true}, {abc: true}); 26 * app.set('foo', 'bar'); 27 * 28 * // values defined with the given `config` object will be on the root of the instance 29 * console.log(app.baz); //=> undefined 30 * console.log(app.foo); //=> 'bar' 31 * // or use `.get` 32 * console.log(app.get('isApp')); //=> true 33 * console.log(app.get('foo')); //=> 'bar' 34 * 35 * // values defined with the given `options` object will be on `app.options 36 * console.log(app.options.abc); //=> true 37 * ``` 38 * 39 * @param {Object} `config` If supplied, this object is passed to [cache-base][] to merge onto the the instance upon instantiation. 40 * @param {Object} `options` If supplied, this object is used to initialize the `base.options` object. 41 * @api public 42 */ 43 44 function Base(config, options) { 45 if (!(this instanceof Base)) { 46 return new Base(config, options); 47 } 48 Cache.call(this, config); 49 this.is('base'); 50 this.initBase(config, options); 51 } 52 53 /** 54 * Inherit cache-base 55 */ 56 57 util.inherits(Base, Cache); 58 59 /** 60 * Add static emitter methods 61 */ 62 63 Emitter(Base); 64 65 /** 66 * Initialize `Base` defaults with the given `config` object 67 */ 68 69 Base.prototype.initBase = function(config, options) { 70 this.options = merge({}, this.options, options); 71 this.cache = this.cache || {}; 72 this.define('registered', {}); 73 if (name) this[name] = {}; 74 75 // make `app._callbacks` non-enumerable 76 this.define('_callbacks', this._callbacks); 77 if (isObject(config)) { 78 this.visit('set', config); 79 } 80 Base.run(this, 'use', fns); 81 }; 82 83 /** 84 * Set the given `name` on `app._name` and `app.is*` properties. Used for doing 85 * lookups in plugins. 86 * 87 * ```js 88 * app.is('foo'); 89 * console.log(app._name); 90 * //=> 'foo' 91 * console.log(app.isFoo); 92 * //=> true 93 * app.is('bar'); 94 * console.log(app.isFoo); 95 * //=> true 96 * console.log(app.isBar); 97 * //=> true 98 * console.log(app._name); 99 * //=> 'bar' 100 * ``` 101 * @name .is 102 * @param {String} `name` 103 * @return {Boolean} 104 * @api public 105 */ 106 107 Base.prototype.is = function(name) { 108 if (typeof name !== 'string') { 109 throw new TypeError('expected name to be a string'); 110 } 111 this.define('is' + pascal(name), true); 112 this.define('_name', name); 113 this.define('_appname', name); 114 return this; 115 }; 116 117 /** 118 * Returns true if a plugin has already been registered on an instance. 119 * 120 * Plugin implementors are encouraged to use this first thing in a plugin 121 * to prevent the plugin from being called more than once on the same 122 * instance. 123 * 124 * ```js 125 * var base = new Base(); 126 * base.use(function(app) { 127 * if (app.isRegistered('myPlugin')) return; 128 * // do stuff to `app` 129 * }); 130 * 131 * // to also record the plugin as being registered 132 * base.use(function(app) { 133 * if (app.isRegistered('myPlugin', true)) return; 134 * // do stuff to `app` 135 * }); 136 * ``` 137 * @name .isRegistered 138 * @emits `plugin` Emits the name of the plugin being registered. Useful for unit tests, to ensure plugins are only registered once. 139 * @param {String} `name` The plugin name. 140 * @param {Boolean} `register` If the plugin if not already registered, to record it as being registered pass `true` as the second argument. 141 * @return {Boolean} Returns true if a plugin is already registered. 142 * @api public 143 */ 144 145 Base.prototype.isRegistered = function(name, register) { 146 if (this.registered.hasOwnProperty(name)) { 147 return true; 148 } 149 if (register !== false) { 150 this.registered[name] = true; 151 this.emit('plugin', name); 152 } 153 return false; 154 }; 155 156 /** 157 * Define a plugin function to be called immediately upon init. Plugins are chainable 158 * and expose the following arguments to the plugin function: 159 * 160 * - `app`: the current instance of `Base` 161 * - `base`: the [first ancestor instance](#base) of `Base` 162 * 163 * ```js 164 * var app = new Base() 165 * .use(foo) 166 * .use(bar) 167 * .use(baz) 168 * ``` 169 * @name .use 170 * @param {Function} `fn` plugin function to call 171 * @return {Object} Returns the item instance for chaining. 172 * @api public 173 */ 174 175 Base.prototype.use = function(fn) { 176 fn.call(this, this); 177 return this; 178 }; 179 180 /** 181 * The `.define` method is used for adding non-enumerable property on the instance. 182 * Dot-notation is **not supported** with `define`. 183 * 184 * ```js 185 * // arbitrary `render` function using lodash `template` 186 * app.define('render', function(str, locals) { 187 * return _.template(str)(locals); 188 * }); 189 * ``` 190 * @name .define 191 * @param {String} `key` The name of the property to define. 192 * @param {any} `value` 193 * @return {Object} Returns the instance for chaining. 194 * @api public 195 */ 196 197 Base.prototype.define = function(key, val) { 198 if (isObject(key)) { 199 return this.visit('define', key); 200 } 201 define(this, key, val); 202 return this; 203 }; 204 205 /** 206 * Mix property `key` onto the Base prototype. If base is inherited using 207 * `Base.extend` this method will be overridden by a new `mixin` method that will 208 * only add properties to the prototype of the inheriting application. 209 * 210 * ```js 211 * app.mixin('foo', function() { 212 * // do stuff 213 * }); 214 * ``` 215 * @name .mixin 216 * @param {String} `key` 217 * @param {Object|Array} `val` 218 * @return {Object} Returns the `base` instance for chaining. 219 * @api public 220 */ 221 222 Base.prototype.mixin = function(key, val) { 223 Base.prototype[key] = val; 224 return this; 225 }; 226 227 /** 228 * Non-enumberable mixin array, used by the static [Base.mixin]() method. 229 */ 230 231 Base.prototype.mixins = Base.prototype.mixins || []; 232 233 /** 234 * Getter/setter used when creating nested instances of `Base`, for storing a reference 235 * to the first ancestor instance. This works by setting an instance of `Base` on the `parent` 236 * property of a "child" instance. The `base` property defaults to the current instance if 237 * no `parent` property is defined. 238 * 239 * ```js 240 * // create an instance of `Base`, this is our first ("base") instance 241 * var first = new Base(); 242 * first.foo = 'bar'; // arbitrary property, to make it easier to see what's happening later 243 * 244 * // create another instance 245 * var second = new Base(); 246 * // create a reference to the first instance (`first`) 247 * second.parent = first; 248 * 249 * // create another instance 250 * var third = new Base(); 251 * // create a reference to the previous instance (`second`) 252 * // repeat this pattern every time a "child" instance is created 253 * third.parent = second; 254 * 255 * // we can always access the first instance using the `base` property 256 * console.log(first.base.foo); 257 * //=> 'bar' 258 * console.log(second.base.foo); 259 * //=> 'bar' 260 * console.log(third.base.foo); 261 * //=> 'bar' 262 * // and now you know how to get to third base ;) 263 * ``` 264 * @name .base 265 * @api public 266 */ 267 268 Object.defineProperty(Base.prototype, 'base', { 269 configurable: true, 270 get: function() { 271 return this.parent ? this.parent.base : this; 272 } 273 }); 274 275 /** 276 * Static method for adding global plugin functions that will 277 * be added to an instance when created. 278 * 279 * ```js 280 * Base.use(function(app) { 281 * app.foo = 'bar'; 282 * }); 283 * var app = new Base(); 284 * console.log(app.foo); 285 * //=> 'bar' 286 * ``` 287 * @name #use 288 * @param {Function} `fn` Plugin function to use on each instance. 289 * @return {Object} Returns the `Base` constructor for chaining 290 * @api public 291 */ 292 293 define(Base, 'use', function(fn) { 294 fns.push(fn); 295 return Base; 296 }); 297 298 /** 299 * Run an array of functions by passing each function 300 * to a method on the given object specified by the given property. 301 * 302 * @param {Object} `obj` Object containing method to use. 303 * @param {String} `prop` Name of the method on the object to use. 304 * @param {Array} `arr` Array of functions to pass to the method. 305 */ 306 307 define(Base, 'run', function(obj, prop, arr) { 308 var len = arr.length, i = 0; 309 while (len--) { 310 obj[prop](arr[i++]); 311 } 312 return Base; 313 }); 314 315 /** 316 * Static method for inheriting the prototype and static methods of the `Base` class. 317 * This method greatly simplifies the process of creating inheritance-based applications. 318 * See [static-extend][] for more details. 319 * 320 * ```js 321 * var extend = cu.extend(Parent); 322 * Parent.extend(Child); 323 * 324 * // optional methods 325 * Parent.extend(Child, { 326 * foo: function() {}, 327 * bar: function() {} 328 * }); 329 * ``` 330 * @name #extend 331 * @param {Function} `Ctor` constructor to extend 332 * @param {Object} `methods` Optional prototype properties to mix in. 333 * @return {Object} Returns the `Base` constructor for chaining 334 * @api public 335 */ 336 337 define(Base, 'extend', cu.extend(Base, function(Ctor, Parent) { 338 Ctor.prototype.mixins = Ctor.prototype.mixins || []; 339 340 define(Ctor, 'mixin', function(fn) { 341 var mixin = fn(Ctor.prototype, Ctor); 342 if (typeof mixin === 'function') { 343 Ctor.prototype.mixins.push(mixin); 344 } 345 return Ctor; 346 }); 347 348 define(Ctor, 'mixins', function(Child) { 349 Base.run(Child, 'mixin', Ctor.prototype.mixins); 350 return Ctor; 351 }); 352 353 Ctor.prototype.mixin = function(key, value) { 354 Ctor.prototype[key] = value; 355 return this; 356 }; 357 return Base; 358 })); 359 360 /** 361 * Used for adding methods to the `Base` prototype, and/or to the prototype of child instances. 362 * When a mixin function returns a function, the returned function is pushed onto the `.mixins` 363 * array, making it available to be used on inheriting classes whenever `Base.mixins()` is 364 * called (e.g. `Base.mixins(Child)`). 365 * 366 * ```js 367 * Base.mixin(function(proto) { 368 * proto.foo = function(msg) { 369 * return 'foo ' + msg; 370 * }; 371 * }); 372 * ``` 373 * @name #mixin 374 * @param {Function} `fn` Function to call 375 * @return {Object} Returns the `Base` constructor for chaining 376 * @api public 377 */ 378 379 define(Base, 'mixin', function(fn) { 380 var mixin = fn(Base.prototype, Base); 381 if (typeof mixin === 'function') { 382 Base.prototype.mixins.push(mixin); 383 } 384 return Base; 385 }); 386 387 /** 388 * Static method for running global mixin functions against a child constructor. 389 * Mixins must be registered before calling this method. 390 * 391 * ```js 392 * Base.extend(Child); 393 * Base.mixins(Child); 394 * ``` 395 * @name #mixins 396 * @param {Function} `Child` Constructor function of a child class 397 * @return {Object} Returns the `Base` constructor for chaining 398 * @api public 399 */ 400 401 define(Base, 'mixins', function(Child) { 402 Base.run(Child, 'mixin', Base.prototype.mixins); 403 return Base; 404 }); 405 406 /** 407 * Similar to `util.inherit`, but copies all static properties, prototype properties, and 408 * getters/setters from `Provider` to `Receiver`. See [class-utils][]{#inherit} for more details. 409 * 410 * ```js 411 * Base.inherit(Foo, Bar); 412 * ``` 413 * @name #inherit 414 * @param {Function} `Receiver` Receiving (child) constructor 415 * @param {Function} `Provider` Providing (parent) constructor 416 * @return {Object} Returns the `Base` constructor for chaining 417 * @api public 418 */ 419 420 define(Base, 'inherit', cu.inherit); 421 define(Base, 'bubble', cu.bubble); 422 return Base; 423 } 424 425 /** 426 * Expose `Base` with default settings 427 */ 428 429 module.exports = namespace(); 430 431 /** 432 * Allow users to define a namespace 433 */ 434 435 module.exports.namespace = namespace;