UNPKG

7.92 kBJavaScriptView Raw
1'use strict';
2
3const assert = require('assert');
4const typeOf = require('kind-of');
5const define = require('define-property');
6const pascal = require('pascalcase');
7const merge = require('mixin-deep');
8const Cache = require('cache-base');
9
10/**
11 * Optionally define a custom `cache` namespace to use.
12 */
13
14function namespace(name) {
15 const fns = [];
16
17 /**
18 * Create an instance of `Base` with the given `cache` and `options`.
19 * Learn about the [cache object](#cache-object).
20 *
21 * ```js
22 * // initialize with `cache` and `options`
23 * const app = new Base({isApp: true}, {abc: true});
24 * app.set('foo', 'bar');
25 *
26 * // values defined with the given `cache` object will be on the root of the instance
27 * console.log(app.baz); //=> undefined
28 * console.log(app.foo); //=> 'bar'
29 * // or use `.get`
30 * console.log(app.get('isApp')); //=> true
31 * console.log(app.get('foo')); //=> 'bar'
32 *
33 * // values defined with the given `options` object will be on `app.options
34 * console.log(app.options.abc); //=> true
35 * ```
36 *
37 * @name Base
38 * @param {Object} `cache` If supplied, this object is passed to [cache-base][] to merge onto the the instance.
39 * @param {Object} `options` If supplied, this object is used to initialize the `base.options` object.
40 * @api public
41 */
42
43 class Base extends Cache {
44 constructor(cache, options) {
45 super(name, cache);
46 this.is('base');
47 this.is('app');
48 this.options = merge({}, this.options, options);
49 this.cache = this.cache || {};
50 this.define('registered', {});
51
52 if (fns.length) {
53 this.use(fns);
54 }
55 }
56
57 /**
58 * Set the given `name` on `app._name` and `app.is*` properties. Used for doing
59 * lookups in plugins.
60 *
61 * ```js
62 * app.is('collection');
63 * console.log(app.type);
64 * //=> 'collection'
65 * console.log(app.isCollection);
66 * //=> true
67 * ```
68 * @name .is
69 * @param {String} `name`
70 * @return {Boolean}
71 * @api public
72 */
73
74 is(type) {
75 assert.equal(typeof type, 'string', 'expected "type" to be a string');
76 if (type !== 'app') delete this.isApp;
77 this.define('type', type.toLowerCase());
78 this.define('is' + pascal(type), true);
79 return this;
80 }
81
82 /**
83 * Returns true if a plugin has already been registered on an instance.
84 *
85 * Plugin implementors are encouraged to use this first thing in a plugin
86 * to prevent the plugin from being called more than once on the same
87 * instance.
88 *
89 * ```js
90 * const base = new Base();
91 * base.use(function(app) {
92 * if (app.isRegistered('myPlugin')) return;
93 * // do stuff to `app`
94 * });
95 *
96 * // to also record the plugin as being registered
97 * base.use(function(app) {
98 * if (app.isRegistered('myPlugin', true)) return;
99 * // do stuff to `app`
100 * });
101 * ```
102 * @name .isRegistered
103 * @emits `plugin` Emits the name of the plugin being registered. Useful for unit tests, to ensure plugins are only registered once.
104 * @param {String} `name` The plugin name.
105 * @param {Boolean} `register` If the plugin if not already registered, to record it as being registered pass `true` as the second argument.
106 * @return {Boolean} Returns true if a plugin is already registered.
107 * @api public
108 */
109
110 isRegistered(name, register) {
111 assert.equal(typeof name, 'string', 'expected name to be a string');
112 if (this.registered.hasOwnProperty(name)) {
113 return true;
114 }
115 if (register !== false) {
116 this.registered[name] = true;
117 this.emit('plugin', name);
118 }
119 return false;
120 }
121
122 /**
123 * Call a plugin function or array of plugin functions on the instance. Plugins
124 * are called with an instance of base, and options (if defined).
125 *
126 * ```js
127 * const app = new Base()
128 * .use([foo, bar])
129 * .use(baz)
130 * ```
131 * @name .use
132 * @param {String|Function|Array} `name` (optional) plugin name
133 * @param {Function|Array} `plugin` plugin function, or array of functions, to call.
134 * @param {...rest} Any additional arguments to pass to plugins(s).
135 * @return {Object} Returns the item instance for chaining.
136 * @api public
137 */
138
139 use(...rest) {
140 let name = null;
141 let fns = null;
142
143 if (typeof rest[0] === 'string') {
144 name = rest.shift();
145 }
146
147 if (typeof rest[0] === 'function' || Array.isArray(rest[0])) {
148 fns = rest.shift();
149 }
150
151 if (Array.isArray(fns)) return fns.forEach(fn => this.use(fn, ...rest));
152 assert.equal(typeof fns, 'function', 'expected plugin to be a function');
153
154 const key = name;
155 if (key && typeof key === 'string' && this.isRegistered(key)) {
156 return this;
157 }
158
159 fns.call(this, this, ...rest);
160 return this;
161 }
162
163 /**
164 * The `.define` method is used for adding non-enumerable property on the instance.
165 * Dot-notation is **not supported** with `define`.
166 *
167 * ```js
168 * // example of a custom arbitrary `render` function created with lodash's `template` method
169 * app.define('render', (str, locals) => _.template(str)(locals));
170 * ```
171 * @name .define
172 * @param {String} `key` The name of the property to define.
173 * @param {any} `value`
174 * @return {Object} Returns the instance for chaining.
175 * @api public
176 */
177
178 define(key, val) {
179 if (typeOf(key) === 'object') {
180 return this.visit('define', key);
181 }
182 define(this, key, val);
183 return this;
184 }
185
186 /**
187 * Getter/setter used when creating nested instances of `Base`, for storing a reference
188 * to the first ancestor instance. This works by setting an instance of `Base` on the `parent`
189 * property of a "child" instance. The `base` property defaults to the current instance if
190 * no `parent` property is defined.
191 *
192 * ```js
193 * // create an instance of `Base`, this is our first ("base") instance
194 * const first = new Base();
195 * first.foo = 'bar'; // arbitrary property, to make it easier to see what's happening later
196 *
197 * // create another instance
198 * const second = new Base();
199 * // create a reference to the first instance (`first`)
200 * second.parent = first;
201 *
202 * // create another instance
203 * const third = new Base();
204 * // create a reference to the previous instance (`second`)
205 * // repeat this pattern every time a "child" instance is created
206 * third.parent = second;
207 *
208 * // we can always access the first instance using the `base` property
209 * console.log(first.base.foo);
210 * //=> 'bar'
211 * console.log(second.base.foo);
212 * //=> 'bar'
213 * console.log(third.base.foo);
214 * //=> 'bar'
215 * ```
216 * @name .base
217 * @api public
218 */
219
220 get base() {
221 return this.parent ? this.parent.base : this;
222 }
223
224 /**
225 * Static method for adding global plugin functions that will
226 * be added to an instance when created.
227 *
228 * ```js
229 * Base.use(function(app) {
230 * app.foo = 'bar';
231 * });
232 * const app = new Base();
233 * console.log(app.foo);
234 * //=> 'bar'
235 * ```
236 * @name #use
237 * @param {Function} `fn` Plugin function to use on each instance.
238 * @return {Object} Returns the `Base` constructor for chaining
239 * @api public
240 */
241
242 static use(fn) {
243 assert.equal(typeof fn, 'function', 'expected plugin to be a function');
244 fns.push(fn);
245 return this;
246 }
247
248 /**
249 * Delete static mixin method from cache-base, JIT
250 */
251
252 static get mixin() {
253 return undefined;
254 }
255 }
256
257 return Base;
258}
259
260/**
261 * Expose `Base` with default settings
262 */
263
264module.exports = namespace();
265
266/**
267 * Allow users to define a namespace
268 */
269
270module.exports.namespace = namespace;