Repository: rollup/rollup
File: src/utils/PluginDriver.ts (TypeScript)

Code before:

		if (basePluginDriver) {
			for (const plugin of userPlugins) {
				for (const hook of inputHooks) {
					if (hook in plugin) {
						options.onwarn(errorInputHookInOutputPlugin(plugin.name, hook));
					}
				}
			}
		}
	}

	public createOutputPluginDriver(plugins: readonly Plugin[]): PluginDriver {
		return new PluginDriver(this.graph, this.options, plugins, this.pluginCache, this);
	}

	getUnfulfilledHookActions(): Set<HookAction> {
		return this.unfulfilledActions;
	}

	// chains, first non-null result stops and returns
	hookFirst<H extends AsyncPluginHooks & FirstPluginHooks>(
		hookName: H,
		parameters: Parameters<FunctionPluginHooks[H]>,
		replaceContext?: ReplaceContext | null,
		skipped?: ReadonlySet<Plugin> | null,
		callBeforeRunHook?: CallBeforeRunHook
	): Promise<ReturnType<FunctionPluginHooks[H]> | null> {
		let promise: Promise<ReturnType<FunctionPluginHooks[H]> | null> = Promise.resolve(null);
		for (const plugin of this.getSortedPlugins(hookName)) {
			if (skipped && skipped.has(plugin)) continue;
			promise = promise.then(result => {
				if (result != null) return result;
				callBeforeRunHook?.(plugin);
				return this.runHook(hookName, parameters, plugin, replaceContext);
			});
		}
		return promise;
	}

	// chains synchronously, first non-null result stops and returns
	hookFirstSync<H extends SyncPluginHooks & FirstPluginHooks>(
		hookName: H,
		parameters: Parameters<FunctionPluginHooks[H]>,
		replaceContext?: ReplaceContext
	): ReturnType<FunctionPluginHooks[H]> | null {
		for (const plugin of this.getSortedPlugins(hookName)) {
			const result = this.runHookSync(hookName, parameters, plugin, replaceContext);
			if (result != null) return result;
		}
		return null;

Review comment (performance):
I wonder about the `callBeforeRunHook`. It is very powerful, but I currently do not see many other uses for it except to figure out the responsible plugin. Moreover, it adds another function call into every resolveId hook, which is one of the most-called hooks. I wonder if it would not be more performant to actually return the plugin from the hook?

Suggestion (which will also improve performance further by breaking out of the loop once we found a handler. This code obviously predates async-await): Add a new handler e.g.

```ts
async hookFirstAndGetPlugin<H extends AsyncPluginHooks & FirstPluginHooks>(
 		hookName: H,
 		parameters: Parameters<FunctionPluginHooks[H]>,
 		replaceContext?: ReplaceContext | null,
 		skipped?: ReadonlySet<Plugin> | null
 	): Promise<[result: ReturnType<FunctionPluginHooks[H]>, plugin: Plugin] | null> {
for (const plugin of this.getSortedPlugins(hookName)) {
			if (skipped?.has(plugin)) continue;
			const result = await this.runHook(hookName, parameters, plugin, replaceContext);
      // Note the single "=" to match both null and undefined
			if (result != null) {
				return [result, plugin];
			}
		}
		return null;
	}
}
```

to avoid changing too many code places, we can define a new `hookFirst` that internally calls `hookFirstAndGetPlugin` and returns the first element if the result is not `null`. What do you think?

Code after:
		if (basePluginDriver) {
			for (const plugin of userPlugins) {
				for (const hook of inputHooks) {
					if (hook in plugin) {
						options.onwarn(errorInputHookInOutputPlugin(plugin.name, hook));
					}
				}
			}
		}
	}

	public createOutputPluginDriver(plugins: readonly Plugin[]): PluginDriver {
		return new PluginDriver(this.graph, this.options, plugins, this.pluginCache, this);
	}

	getUnfulfilledHookActions(): Set<HookAction> {
		return this.unfulfilledActions;
	}

	// chains, first non-null result stops and returns
	hookFirst<H extends AsyncPluginHooks & FirstPluginHooks>(
		hookName: H,
		parameters: Parameters<FunctionPluginHooks[H]>,
		replaceContext?: ReplaceContext | null,
		skipped?: ReadonlySet<Plugin> | null
	): Promise<ReturnType<FunctionPluginHooks[H]> | null> {
		return this.hookFirstAndGetPlugin(hookName, parameters, replaceContext, skipped).then(
			result => result && result[0]
		);
	}

	// chains, first non-null result stops and returns result and last plugin
	async hookFirstAndGetPlugin<H extends AsyncPluginHooks & FirstPluginHooks>(
		hookName: H,
		parameters: Parameters<FunctionPluginHooks[H]>,
		replaceContext?: ReplaceContext | null,
		skipped?: ReadonlySet<Plugin> | null
	): Promise<[NonNullable<ReturnType<FunctionPluginHooks[H]>>, Plugin] | null> {
		for (const plugin of this.getSortedPlugins(hookName)) {
			if (skipped?.has(plugin)) continue;
			const result = await this.runHook(hookName, parameters, plugin, replaceContext);
			if (result != null) return [result, plugin];
		}
		return null;
	}

	// chains synchronously, first non-null result stops and returns
	hookFirstSync<H extends SyncPluginHooks & FirstPluginHooks>(
		hookName: H,
		parameters: Parameters<FunctionPluginHooks[H]>,
		replaceContext?: ReplaceContext