diff --git a/src/utils/hookActions.ts b/src/utils/hookActions.ts index ef07992eb9d..c2143332a00 100644 --- a/src/utils/hookActions.ts +++ b/src/utils/hookActions.ts @@ -1,4 +1,3 @@ -import { EventEmitter } from 'node:events'; import process from 'node:process'; import type { HookAction, PluginDriver } from './PluginDriver'; @@ -25,33 +24,40 @@ function formatAction([pluginName, hookName, parameters]: HookAction): string { return action; } -// We do not directly listen on process to avoid max listeners warnings for -// complicated build processes -const beforeExitEvent = 'beforeExit'; -// eslint-disable-next-line unicorn/prefer-event-target -const beforeExitEmitter = new EventEmitter(); -beforeExitEmitter.setMaxListeners(0); -process.on(beforeExitEvent, () => beforeExitEmitter.emit(beforeExitEvent)); +let handleBeforeExit: null | (() => void) = null; +const rejectByPluginDriver = new Map void>(); export async function catchUnfinishedHookActions( pluginDriver: PluginDriver, callback: () => Promise ): Promise { - let handleEmptyEventLoop: () => void; const emptyEventLoopPromise = new Promise((_, reject) => { - handleEmptyEventLoop = () => { - const unfulfilledActions = pluginDriver.getUnfulfilledHookActions(); - reject( - new Error( - `Unexpected early exit. This happens when Promises returned by plugins cannot resolve. Unfinished hook action(s) on exit:\n` + - [...unfulfilledActions].map(formatAction).join('\n') - ) - ); - }; - beforeExitEmitter.once(beforeExitEvent, handleEmptyEventLoop); + rejectByPluginDriver.set(pluginDriver, reject); + if (!handleBeforeExit) { + // We only ever create a single event listener to avoid max listener and + // other issues + handleBeforeExit = () => { + for (const [pluginDriver, reject] of rejectByPluginDriver) { + const unfulfilledActions = pluginDriver.getUnfulfilledHookActions(); + reject( + new Error( + `Unexpected early exit. This happens when Promises returned by plugins cannot resolve. Unfinished hook action(s) on exit:\n` + + [...unfulfilledActions].map(formatAction).join('\n') + ) + ); + } + }; + process.once('beforeExit', handleBeforeExit); + } }); - const result = await Promise.race([callback(), emptyEventLoopPromise]); - beforeExitEmitter.off(beforeExitEvent, handleEmptyEventLoop!); - return result; + try { + return await Promise.race([callback(), emptyEventLoopPromise]); + } finally { + rejectByPluginDriver.delete(pluginDriver); + if (rejectByPluginDriver.size === 0) { + process.off('beforeExit', handleBeforeExit!); + handleBeforeExit = null; + } + } }