1 | ;
|
2 |
|
3 | const assert = require('assert');
|
4 | const typeOf = require('kind-of');
|
5 | const define = require('define-property');
|
6 | const pascal = require('pascalcase');
|
7 | const merge = require('mixin-deep');
|
8 | const Cache = require('cache-base');
|
9 |
|
10 | /**
|
11 | * Optionally define a custom `cache` namespace to use.
|
12 | */
|
13 |
|
14 | function 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 |
|
264 | module.exports = namespace();
|
265 |
|
266 | /**
|
267 | * Allow users to define a namespace
|
268 | */
|
269 |
|
270 | module.exports.namespace = namespace;
|