You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					286 lines
				
				6.5 KiB
			
		
		
			
		
	
	
					286 lines
				
				6.5 KiB
			| 
											3 years ago
										 | 'use strict' | ||
|  | 
 | ||
|  | const queueMicrotask = require('queue-microtask') | ||
|  | const fastq = require('fastq') | ||
|  | const EE = require('events').EventEmitter | ||
|  | const inherits = require('util').inherits | ||
|  | const debug = require('debug')('avvio') | ||
|  | const CODE_PLUGIN_TIMEOUT = 'ERR_AVVIO_PLUGIN_TIMEOUT' | ||
|  | 
 | ||
|  | function getName (func) { | ||
|  |   // let's see if this is a file, and in that case use that
 | ||
|  |   // this is common for plugins
 | ||
|  |   const cache = require.cache | ||
|  |   const keys = Object.keys(cache) | ||
|  | 
 | ||
|  |   // eslint-disable-next-line no-var
 | ||
|  |   for (var i = 0; i < keys.length; i++) { | ||
|  |     if (cache[keys[i]].exports === func) { | ||
|  |       return keys[i] | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // if not maybe it's a named function, so use that
 | ||
|  |   if (func.name) { | ||
|  |     return func.name | ||
|  |   } | ||
|  | 
 | ||
|  |   // takes the first two lines of the function if nothing else works
 | ||
|  |   return func.toString().split('\n').slice(0, 2).map(s => s.trim()).join(' -- ') | ||
|  | } | ||
|  | 
 | ||
|  | function promise () { | ||
|  |   const obj = {} | ||
|  | 
 | ||
|  |   obj.promise = new Promise((resolve, reject) => { | ||
|  |     obj.resolve = resolve | ||
|  |     obj.reject = reject | ||
|  |   }) | ||
|  | 
 | ||
|  |   return obj | ||
|  | } | ||
|  | 
 | ||
|  | function Plugin (parent, func, optsOrFunc, isAfter, timeout) { | ||
|  |   this.started = false | ||
|  |   this.func = func | ||
|  |   this.opts = optsOrFunc | ||
|  |   this.onFinish = null | ||
|  |   this.parent = parent | ||
|  |   this.timeout = timeout === undefined ? parent._timeout : timeout | ||
|  |   this.name = getName(func) | ||
|  |   this.isAfter = isAfter | ||
|  |   this.q = fastq(parent, loadPluginNextTick, 1) | ||
|  |   this.q.pause() | ||
|  |   this._error = null | ||
|  |   this.loaded = false | ||
|  |   this._promise = null | ||
|  | 
 | ||
|  |   // always start the queue in the next tick
 | ||
|  |   // because we try to attach subsequent call to use()
 | ||
|  |   // to the right plugin. we need to defer them,
 | ||
|  |   // or they will end up at the top of _current
 | ||
|  | } | ||
|  | 
 | ||
|  | inherits(Plugin, EE) | ||
|  | 
 | ||
|  | Plugin.prototype.exec = function (server, cb) { | ||
|  |   const func = this.func | ||
|  |   let completed = false | ||
|  |   const name = this.name | ||
|  | 
 | ||
|  |   if (this.parent._error && !this.isAfter) { | ||
|  |     debug('skipping loading of plugin as parent errored and it is not an after', name) | ||
|  |     process.nextTick(cb) | ||
|  |     return | ||
|  |   } | ||
|  | 
 | ||
|  |   if (!this.isAfter) { | ||
|  |     // Skip override for after
 | ||
|  |     try { | ||
|  |       this.server = this.parent.override(server, func, this.opts) | ||
|  |     } catch (err) { | ||
|  |       debug('override errored', name) | ||
|  |       return cb(err) | ||
|  |     } | ||
|  |   } else { | ||
|  |     this.server = server | ||
|  |   } | ||
|  | 
 | ||
|  |   this.opts = typeof this.opts === 'function' ? this.opts(this.server) : this.opts | ||
|  | 
 | ||
|  |   debug('exec', name) | ||
|  | 
 | ||
|  |   let timer | ||
|  | 
 | ||
|  |   const done = (err) => { | ||
|  |     if (completed) { | ||
|  |       debug('loading complete', name) | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     this._error = err | ||
|  | 
 | ||
|  |     if (err) { | ||
|  |       debug('exec errored', name) | ||
|  |     } else { | ||
|  |       debug('exec completed', name) | ||
|  |     } | ||
|  | 
 | ||
|  |     completed = true | ||
|  | 
 | ||
|  |     if (timer) { | ||
|  |       clearTimeout(timer) | ||
|  |     } | ||
|  | 
 | ||
|  |     cb(err) | ||
|  |   } | ||
|  | 
 | ||
|  |   if (this.timeout > 0) { | ||
|  |     debug('setting up timeout', name, this.timeout) | ||
|  |     timer = setTimeout(function () { | ||
|  |       debug('timed out', name) | ||
|  |       timer = null | ||
|  |       const err = new Error(`${CODE_PLUGIN_TIMEOUT}: plugin did not start in time: ${name}. You may have forgotten to call 'done' function or to resolve a Promise`) | ||
|  |       err.code = CODE_PLUGIN_TIMEOUT | ||
|  |       err.fn = func | ||
|  |       done(err) | ||
|  |     }, this.timeout) | ||
|  |   } | ||
|  | 
 | ||
|  |   this.started = true | ||
|  |   this.emit('start', this.server ? this.server.name : null, this.name, Date.now()) | ||
|  |   const promise = func(this.server, this.opts, done) | ||
|  | 
 | ||
|  |   if (promise && typeof promise.then === 'function') { | ||
|  |     debug('exec: resolving promise', name) | ||
|  | 
 | ||
|  |     promise.then( | ||
|  |       () => process.nextTick(done), | ||
|  |       (e) => process.nextTick(done, e)) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | Plugin.prototype.loadedSoFar = function () { | ||
|  |   if (this.loaded) { | ||
|  |     return Promise.resolve() | ||
|  |   } | ||
|  | 
 | ||
|  |   const setup = () => { | ||
|  |     this.server.after((err, cb) => { | ||
|  |       this._error = err | ||
|  |       this.q.pause() | ||
|  | 
 | ||
|  |       if (err) { | ||
|  |         debug('rejecting promise', this.name, err) | ||
|  |         this._promise.reject(err) | ||
|  |       } else { | ||
|  |         debug('resolving promise', this.name) | ||
|  |         this._promise.resolve() | ||
|  |       } | ||
|  |       this._promise = null | ||
|  | 
 | ||
|  |       process.nextTick(cb, err) | ||
|  |     }) | ||
|  |     this.q.resume() | ||
|  |   } | ||
|  | 
 | ||
|  |   let res | ||
|  | 
 | ||
|  |   if (!this._promise) { | ||
|  |     this._promise = promise() | ||
|  |     res = this._promise.promise | ||
|  | 
 | ||
|  |     if (!this.server) { | ||
|  |       this.on('start', setup) | ||
|  |     } else { | ||
|  |       setup() | ||
|  |     } | ||
|  |   } else { | ||
|  |     res = Promise.resolve() | ||
|  |   } | ||
|  | 
 | ||
|  |   return res | ||
|  | } | ||
|  | 
 | ||
|  | Plugin.prototype.enqueue = function (obj, cb) { | ||
|  |   debug('enqueue', this.name, obj.name) | ||
|  |   this.emit('enqueue', this.server ? this.server.name : null, this.name, Date.now()) | ||
|  |   this.q.push(obj, cb) | ||
|  | } | ||
|  | 
 | ||
|  | Plugin.prototype.finish = function (err, cb) { | ||
|  |   debug('finish', this.name, err) | ||
|  |   const done = () => { | ||
|  |     if (this.loaded) { | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     debug('loaded', this.name) | ||
|  |     this.emit('loaded', this.server ? this.server.name : null, this.name, Date.now()) | ||
|  |     this.loaded = true | ||
|  | 
 | ||
|  |     cb(err) | ||
|  |   } | ||
|  | 
 | ||
|  |   if (err) { | ||
|  |     if (this._promise) { | ||
|  |       this._promise.reject(err) | ||
|  |       this._promise = null | ||
|  |     } | ||
|  |     done() | ||
|  |     return | ||
|  |   } | ||
|  | 
 | ||
|  |   const check = () => { | ||
|  |     debug('check', this.name, this.q.length(), this.q.running(), this._promise) | ||
|  |     if (this.q.length() === 0 && this.q.running() === 0) { | ||
|  |       if (this._promise) { | ||
|  |         const wrap = () => { | ||
|  |           debug('wrap') | ||
|  |           queueMicrotask(check) | ||
|  |         } | ||
|  |         this._promise.resolve() | ||
|  |         this._promise.promise.then(wrap, wrap) | ||
|  |         this._promise = null | ||
|  |       } else { | ||
|  |         done() | ||
|  |       } | ||
|  |     } else { | ||
|  |       debug('delayed', this.name) | ||
|  |       // finish when the queue of nested plugins to load is empty
 | ||
|  |       this.q.drain = () => { | ||
|  |         debug('drain', this.name) | ||
|  |         this.q.drain = noop | ||
|  | 
 | ||
|  |         // we defer the check, as a safety net for things
 | ||
|  |         // that might be scheduled in the loading callback
 | ||
|  |         queueMicrotask(check) | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   queueMicrotask(check) | ||
|  | 
 | ||
|  |   // we start loading the dependents plugins only once
 | ||
|  |   // the current level is finished
 | ||
|  |   this.q.resume() | ||
|  | } | ||
|  | 
 | ||
|  | // delays plugin loading until the next tick to ensure any bound `_after` callbacks have a chance
 | ||
|  | // to run prior to executing the next plugin
 | ||
|  | function loadPluginNextTick (toLoad, cb) { | ||
|  |   const parent = this | ||
|  |   process.nextTick(loadPlugin.bind(parent), toLoad, cb) | ||
|  | } | ||
|  | 
 | ||
|  | // loads a plugin
 | ||
|  | function loadPlugin (toLoad, cb) { | ||
|  |   if (typeof toLoad.func.then === 'function') { | ||
|  |     toLoad.func.then((fn) => { | ||
|  |       if (typeof fn.default === 'function') { | ||
|  |         fn = fn.default | ||
|  |       } | ||
|  |       toLoad.func = fn | ||
|  |       loadPlugin.call(this, toLoad, cb) | ||
|  |     }, cb) | ||
|  |     return | ||
|  |   } | ||
|  | 
 | ||
|  |   const last = this._current[0] | ||
|  | 
 | ||
|  |   // place the plugin at the top of _current
 | ||
|  |   this._current.unshift(toLoad) | ||
|  | 
 | ||
|  |   toLoad.exec((last && last.server) || this._server, (err) => { | ||
|  |     toLoad.finish(err, (err) => { | ||
|  |       this._current.shift() | ||
|  |       cb(err) | ||
|  |     }) | ||
|  |   }) | ||
|  | } | ||
|  | 
 | ||
|  | function noop () {} | ||
|  | 
 | ||
|  | module.exports = Plugin | ||
|  | module.exports.loadPlugin = loadPlugin |