Skip to content

Commit

Permalink
Do not force quit Rollup or close stdout (#5004)
Browse files Browse the repository at this point in the history
* Do not force quit Rollup or close stdout

reverts #4969, #4983

* Avoid memory leaks via beforeExit listeners
  • Loading branch information
lukastaegert committed May 21, 2023
1 parent 0fbb707 commit 020ccc3
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 27 deletions.
6 changes: 2 additions & 4 deletions cli/cli.ts
Expand Up @@ -21,8 +21,6 @@ if (command.help || (process.argv.length <= 2 && process.stdin.isTTY)) {
} catch {
// do nothing
}
run(command).then(() => {
process.stdout.on('finish', () => process.exit(0));
process.stdout.end();
});

run(command);
}
11 changes: 10 additions & 1 deletion rollup.config.ts
Expand Up @@ -137,7 +137,16 @@ export default async function (
terser({ module: true, output: { comments: 'some' } }),
collectLicensesBrowser(),
writeLicenseBrowser(),
cleanBeforeWrite('browser/dist')
cleanBeforeWrite('browser/dist'),
{
closeBundle() {
// On CI, macOS runs sometimes do not close properly. This is a hack
// to fix this until the problem is understood.
console.log('Force quit.');
setTimeout(() => process.exit(0));
},
name: 'force-close'
}
],
strictDeprecations: true,
treeshake
Expand Down
50 changes: 28 additions & 22 deletions src/utils/hookActions.ts
@@ -1,4 +1,3 @@
import { EventEmitter } from 'node:events';
import process from 'node:process';
import type { HookAction, PluginDriver } from './PluginDriver';

Expand All @@ -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<PluginDriver, (reason: Error) => void>();

export async function catchUnfinishedHookActions<T>(
pluginDriver: PluginDriver,
callback: () => Promise<T>
): Promise<T> {
let handleEmptyEventLoop: () => void;
const emptyEventLoopPromise = new Promise<T>((_, 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;
}
}
}

0 comments on commit 020ccc3

Please sign in to comment.