jahed.dev

Dealing with Global Hooks in html-webpack-plugin@4

I have a plugin called webpack-html-meta (WHM) that uses html-webpack-plugin (HWP) to inject <meta /> assets (icons, manifests, etc.) generated using favicons. HWP released a new major version (v4) which changed how other plugins hooks into its pipeline. So I decided to update WHM to match before it falls behind.

It wasn't difficult, it's mostly moving from one function call to a different one. The problem was that HWP moved its hooks from the compilation instance generated during Webpack builds to its own global namespace accessed through a module import.

import HtmlWebpackPlugin from "html-webpack-plugin";
HtmlWebpackPlugin.getHooks().beforeEmit.tapAsync(/* etc. */);

Isolation is a good idea to avoid conflicts, but globals are never a good idea. Especially when there's a chance of conflicting dependencies. Usually, there'll only be one HWP module, but being a node_modules dependency, there can be multiple nested under other modules. When you have multiple modules, you have multiple, different globals with no way to know which one is actually used by Webpack. peerDependencies can be used to avoid installing multiple, but even then, if you're using devDependencies alongside npm link that can still cause problems.

To work around this, I made an ugly function that looks for a HtmlWebpackPlugin instance in the current build and traverses up to its constructor to find the exact module the plugin is using.

import type { Compiler } from "webpack";
import type { default as HtmlWebpackPluginInstance } from "html-webpack-plugin";

const extractHtmlWebpackPluginModule = (
  compiler: Compiler
): typeof HtmlWebpackPluginInstance | null => {
  const htmlWebpackPlugin = (compiler.options.plugins || []).find(
    (plugin) => plugin.constructor.name === "HtmlWebpackPlugin"
  ) as typeof HtmlWebpackPluginInstance | undefined;
  if (!htmlWebpackPlugin) {
    return null;
  }
  const HtmlWebpackPlugin = htmlWebpackPlugin.constructor;
  if (!HtmlWebpackPlugin || !("getHooks" in HtmlWebpackPlugin)) {
    return null;
  }
  return HtmlWebpackPlugin as typeof HtmlWebpackPluginInstance;
};

// ...

const HtmlWebpackPlugin = extractHtmlWebpackPluginModule(compiler);
if (!HtmlWebpackPlugin) {
  throw new Error("Plugin needs to be used with html-webpack-plugin@4");
}
HtmlWebpackPlugin.getHooks().beforeEmit.tapAsync(/* etc. */);

Not ideal, but that's what happens when globals are accessed directly through module imports.

A Proper Solution?

The only real solution I can think of to this problem is to pass through the HWP instance to WHM. This would also allow plugins to elegantly hook into different HTML outputs. But the HWP author makes a good point about people using Webpack's CLI where passing instances around isn't possible.

I guess this comes back to how popular libraries can get a bit stuck in satisfying the needs of too many usecases. Personally? I would get rid of the ability to add plugins through the CLI. I've never seen a project use it. But that's just me.

Thanks for reading.