| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- /**
- * @typedef {import('./types.d.ts').PluginDefinition} PluginDefinition
- */
- /**
- * Provides a way to load "plugins" as provided by the user.
- *
- * Currently supports:
- *
- * - Root hooks
- * - Global fixtures (setup/teardown)
- * @private
- * @module plugin
- */
- 'use strict';
- const debug = require('debug')('mocha:plugin-loader');
- const {
- createInvalidPluginDefinitionError,
- createInvalidPluginImplementationError
- } = require('./errors');
- const {castArray} = require('./utils');
- /**
- * @typedef {import('./types.d.ts').PluginLoaderOptions} PluginLoaderOptions
- */
- /**
- * 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;
|