Spaces:
Paused
Paused
| /** | |
| * Provides a way to load "plugins" as provided by the user. | |
| * | |
| * Currently supports: | |
| * | |
| * - Root hooks | |
| * - Global fixtures (setup/teardown) | |
| * @private | |
| * @module plugin | |
| */ | |
| ; | |
| const debug = require('debug')('mocha:plugin-loader'); | |
| const { | |
| createInvalidPluginDefinitionError, | |
| createInvalidPluginImplementationError | |
| } = require('./errors'); | |
| const {castArray} = require('./utils'); | |
| /** | |
| * Built-in plugin definitions. | |
| */ | |
| const MochaPlugins = [ | |
| /** | |
| * Root hook plugin definition | |
| * @type {PluginDefinition} | |
| */ | |
| { | |
| exportName: 'mochaHooks', | |
| optionName: 'rootHooks', | |
| validate(value) { | |
| if ( | |
| Array.isArray(value) || | |
| (typeof value !== 'function' && typeof value !== 'object') | |
| ) { | |
| throw createInvalidPluginImplementationError( | |
| `mochaHooks must be an object or a function returning (or fulfilling with) an object` | |
| ); | |
| } | |
| }, | |
| async finalize(rootHooks) { | |
| if (rootHooks.length) { | |
| const rootHookObjects = await Promise.all( | |
| rootHooks.map(async hook => | |
| typeof hook === 'function' ? hook() : hook | |
| ) | |
| ); | |
| return rootHookObjects.reduce( | |
| (acc, hook) => { | |
| hook = { | |
| beforeAll: [], | |
| beforeEach: [], | |
| afterAll: [], | |
| afterEach: [], | |
| ...hook | |
| }; | |
| return { | |
| beforeAll: [...acc.beforeAll, ...castArray(hook.beforeAll)], | |
| beforeEach: [...acc.beforeEach, ...castArray(hook.beforeEach)], | |
| afterAll: [...acc.afterAll, ...castArray(hook.afterAll)], | |
| afterEach: [...acc.afterEach, ...castArray(hook.afterEach)] | |
| }; | |
| }, | |
| {beforeAll: [], beforeEach: [], afterAll: [], afterEach: []} | |
| ); | |
| } | |
| } | |
| }, | |
| /** | |
| * Global setup fixture plugin definition | |
| * @type {PluginDefinition} | |
| */ | |
| { | |
| exportName: 'mochaGlobalSetup', | |
| optionName: 'globalSetup', | |
| validate(value) { | |
| let isValid = true; | |
| if (Array.isArray(value)) { | |
| if (value.some(item => typeof item !== 'function')) { | |
| isValid = false; | |
| } | |
| } else if (typeof value !== 'function') { | |
| isValid = false; | |
| } | |
| if (!isValid) { | |
| throw createInvalidPluginImplementationError( | |
| `mochaGlobalSetup must be a function or an array of functions`, | |
| {pluginDef: this, pluginImpl: value} | |
| ); | |
| } | |
| } | |
| }, | |
| /** | |
| * Global teardown fixture plugin definition | |
| * @type {PluginDefinition} | |
| */ | |
| { | |
| exportName: 'mochaGlobalTeardown', | |
| optionName: 'globalTeardown', | |
| validate(value) { | |
| let isValid = true; | |
| if (Array.isArray(value)) { | |
| if (value.some(item => typeof item !== 'function')) { | |
| isValid = false; | |
| } | |
| } else if (typeof value !== 'function') { | |
| isValid = false; | |
| } | |
| if (!isValid) { | |
| throw createInvalidPluginImplementationError( | |
| `mochaGlobalTeardown must be a function or an array of functions`, | |
| {pluginDef: this, pluginImpl: value} | |
| ); | |
| } | |
| } | |
| } | |
| ]; | |
| /** | |
| * Contains a registry of [plugin definitions]{@link PluginDefinition} and discovers plugin implementations in user-supplied code. | |
| * | |
| * - [load()]{@link #load} should be called for all required modules | |
| * - The result of [finalize()]{@link #finalize} should be merged into the options for the [Mocha]{@link Mocha} constructor. | |
| * @private | |
| */ | |
| class PluginLoader { | |
| /** | |
| * Initializes plugin names, plugin map, etc. | |
| * @param {PluginLoaderOptions} [opts] - Options | |
| */ | |
| constructor({pluginDefs = MochaPlugins, ignore = []} = {}) { | |
| /** | |
| * Map of registered plugin defs | |
| * @type {Map<string,PluginDefinition>} | |
| */ | |
| this.registered = new Map(); | |
| /** | |
| * Cache of known `optionName` values for checking conflicts | |
| * @type {Set<string>} | |
| */ | |
| this.knownOptionNames = new Set(); | |
| /** | |
| * Cache of known `exportName` values for checking conflicts | |
| * @type {Set<string>} | |
| */ | |
| this.knownExportNames = new Set(); | |
| /** | |
| * Map of user-supplied plugin implementations | |
| * @type {Map<string,Array<*>>} | |
| */ | |
| this.loaded = new Map(); | |
| /** | |
| * Set of ignored plugins by export name | |
| * @type {Set<string>} | |
| */ | |
| this.ignoredExportNames = new Set(castArray(ignore)); | |
| castArray(pluginDefs).forEach(pluginDef => { | |
| this.register(pluginDef); | |
| }); | |
| debug( | |
| 'registered %d plugin defs (%d ignored)', | |
| this.registered.size, | |
| this.ignoredExportNames.size | |
| ); | |
| } | |
| /** | |
| * Register a plugin | |
| * @param {PluginDefinition} pluginDef - Plugin definition | |
| */ | |
| register(pluginDef) { | |
| if (!pluginDef || typeof pluginDef !== 'object') { | |
| throw createInvalidPluginDefinitionError( | |
| 'pluginDef is non-object or falsy', | |
| pluginDef | |
| ); | |
| } | |
| if (!pluginDef.exportName) { | |
| throw createInvalidPluginDefinitionError( | |
| `exportName is expected to be a non-empty string`, | |
| pluginDef | |
| ); | |
| } | |
| let {exportName} = pluginDef; | |
| if (this.ignoredExportNames.has(exportName)) { | |
| debug( | |
| 'refusing to register ignored plugin with export name "%s"', | |
| exportName | |
| ); | |
| return; | |
| } | |
| exportName = String(exportName); | |
| pluginDef.optionName = String(pluginDef.optionName || exportName); | |
| if (this.knownExportNames.has(exportName)) { | |
| throw createInvalidPluginDefinitionError( | |
| `Plugin definition conflict: ${exportName}; exportName must be unique`, | |
| pluginDef | |
| ); | |
| } | |
| this.loaded.set(exportName, []); | |
| this.registered.set(exportName, pluginDef); | |
| this.knownExportNames.add(exportName); | |
| this.knownOptionNames.add(pluginDef.optionName); | |
| debug('registered plugin def "%s"', exportName); | |
| } | |
| /** | |
| * Inspects a module's exports for known plugins and keeps them in memory. | |
| * | |
| * @param {*} requiredModule - The exports of a module loaded via `--require` | |
| * @returns {boolean} If one or more plugins was found, return `true`. | |
| */ | |
| load(requiredModule) { | |
| // we should explicitly NOT fail if other stuff is exported. | |
| // we only care about the plugins we know about. | |
| if (requiredModule && typeof requiredModule === 'object') { | |
| return Array.from(this.knownExportNames).reduce( | |
| (pluginImplFound, pluginName) => { | |
| const pluginImpl = requiredModule[pluginName]; | |
| if (pluginImpl) { | |
| const plugin = this.registered.get(pluginName); | |
| if (typeof plugin.validate === 'function') { | |
| plugin.validate(pluginImpl); | |
| } | |
| this.loaded.set(pluginName, [ | |
| ...this.loaded.get(pluginName), | |
| ...castArray(pluginImpl) | |
| ]); | |
| return true; | |
| } | |
| return pluginImplFound; | |
| }, | |
| false | |
| ); | |
| } | |
| return false; | |
| } | |
| /** | |
| * Call the `finalize()` function of each known plugin definition on the plugins found by [load()]{@link PluginLoader#load}. | |
| * | |
| * Output suitable for passing as input into {@link Mocha} constructor. | |
| * @returns {Promise<object>} Object having keys corresponding to registered plugin definitions' `optionName` prop (or `exportName`, if none), and the values are the implementations as provided by a user. | |
| */ | |
| async finalize() { | |
| const finalizedPlugins = Object.create(null); | |
| for await (const [exportName, pluginImpls] of this.loaded.entries()) { | |
| if (pluginImpls.length) { | |
| const plugin = this.registered.get(exportName); | |
| finalizedPlugins[plugin.optionName] = | |
| typeof plugin.finalize === 'function' | |
| ? await plugin.finalize(pluginImpls) | |
| : pluginImpls; | |
| } | |
| } | |
| debug('finalized plugins: %O', finalizedPlugins); | |
| return finalizedPlugins; | |
| } | |
| /** | |
| * Constructs a {@link PluginLoader} | |
| * @param {PluginLoaderOptions} [opts] - Plugin loader options | |
| */ | |
| static create({pluginDefs = MochaPlugins, ignore = []} = {}) { | |
| return new PluginLoader({pluginDefs, ignore}); | |
| } | |
| } | |
| module.exports = PluginLoader; | |
| /** | |
| * Options for {@link PluginLoader} | |
| * @typedef {Object} PluginLoaderOptions | |
| * @property {PluginDefinition[]} [pluginDefs] - Plugin definitions | |
| * @property {string[]} [ignore] - A list of plugins to ignore when loading | |
| */ | |