Library / SDK
webpack/tapable avatar
webpack/tapable

Tapable: the hook system behind webpack plugins

Just a little module for plugins.

3,861 stars390 forksJavaScriptMIT

At a glance

What is it?
Tapable is the small MIT-licensed module that gives JavaScript classes a typed hook API, and it is the mechanism webpack builds its plugin surface on. It suits library authors who need ordered, interceptable extension points more than application code.
Who is it for?
Adopt Tapable if you are writing a library or build tool that needs named, ordered extension points and you are willing to expose a hooks property as the public surface. Do not reach for it in application code with a single callback, where a plain function or an EventEmitter is less machinery.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem Tapable solves for plugin authors

A library that wants to be extensible has to answer an awkward question: how do consumers add behaviour at points the library controls, without the library knowing who they are? Passing a callback works for one extension point. Ten extension points, each with several subscribers, and you end up writing your own registration, ordering and error handling. Tapable packages that up. The README describes it as a module that "exposes many Hook classes, which can be used to create hooks for plugins."

The audience is narrow and specific. It is for people writing the host: a bundler, a task runner, a framework. The README's own example is a Car class that publishes accelerate, brake and calculateRoutes hooks on a hooks property, and the stated best practice is exactly that, exposing all hooks of a class in one place. Consumers then call tap on those hooks. If you are the consumer rather than the host, you rarely import Tapable directly; you write plugins against somebody else's hooks.

Nine hook classes and what each one does with return values

The package exports nine Hook classes. They split along two axes. Sync or async is the first: SyncHook, SyncBailHook, SyncLoopHook and SyncWaterfallHook are synchronous, while AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook and AsyncSeriesWaterfallHook are not. The second axis is what happens to return values, and this is where people get caught out.

A plain SyncHook discards returns. The README is blunt about it: calling a SyncHook returns undefined "even when you returned values". SyncWaterfallHook passes each tap's return value into the next tap, which is how you build a pipeline where plugins transform a value in sequence. Bail hooks stop at the first tap that returns something, which is how you express "first plugin that handles this wins". Loop hooks re-run taps while they return a truthy value. AsyncParallelHook runs all taps at once and gives no result; AsyncSeriesWaterfallHook runs them in order and threads the value through.

Getting this wrong is the most common mistake. If you need a value back, the README says to use SyncWaterfallHook or AsyncSeriesWaterfallHook respectively. Choosing a plain hook and expecting a return is not a subtle bug, it is a silently ignored value.

Ordering taps with stage and before

Registration order is the default, but Tapable lets a tap declare where it wants to sit. A tap can be registered with a string name or with an options object carrying name, stage and before. Lower stages run earlier, the default is 0, and taps at the same stage fall back to registration order. The before option takes a string or an array of strings and inserts the tap ahead of the named taps; the README notes that unknown names are ignored rather than throwing, which means a typo in before fails quietly.

When stage and before disagree, before wins for the taps it targets, and everything else is still ordered by stage. That rule is stated plainly in the options table, and it is the kind of detail worth reading twice before you build ordering assumptions into a plugin ecosystem.

The withOptions method is the useful part for library authors. It returns a facade around a hook whose tap methods merge a fixed options object into every registration. The README's example creates a facade with stage 10 so all taps registered through it run last, and another with stage -10 so they run first. Per-tap options override the facade. The facade deliberately does not expose the call methods, so it is safe to hand to plugins: they can register, they cannot fire your hooks.

Installing Tapable and registering your first hook

The README gives a single install command. It is a normal npm package with no build step for consumers; the published files are the lib directory and a TypeScript declaration file, so editors pick up types without extra configuration.

bash
npm install --save tapable

The README's own example is a Car class that owns its hooks and calls them. The constructor takes a list of argument names as strings, which is metadata the hook uses when it compiles its runner. A plugin then registers with tap, passing a name (the README says a name is required, and it is used for debugging, interceptors and the before option) and a function.

javascript
const {
	AsyncParallelBailHook,
	AsyncParallelHook,
	AsyncSeriesBailHook,
	AsyncSeriesHook,
	AsyncSeriesWaterfallHook,
	SyncBailHook,
	SyncHook,
	SyncLoopHook,
	SyncWaterfallHook
} = require("tapable");

With SyncHook imported, the README constructs a hook with named arguments and registers a consumer. The second argument to tap is the plugin name; the third is the callback, which here receives the argument the hook was declared with.

javascript
const hook = new SyncHook(["arg1", "arg2", "arg3"]);

For asynchronous work the hook class has to match. An AsyncParallelHook accepts tapPromise with a function returning a promise, and tapAsync with a function whose last argument is a node-style callback. The README's navigation example registers both against the same hook, plus a plain sync tap, which is allowed on async hooks.

javascript
myCar.hooks.calculateRoutes.tapPromise(
	"GoogleMapsPlugin",
	(source, target, routesList) =>
		// return a promise
		google.maps.findRoute(source, target).then((route) => {
			routesList.add(route);
		})
);

One constraint from the plugin API section: if a tapPromise callback returns something that is not thenable, the hook throws. That is a deliberate check rather than a silent no-op.

Where Tapable is the wrong tool

Tapable is infrastructure for a host library, and it shows. The README's own framing is that the class declaring hooks is responsible for calling them; Tapable does not discover plugins, load them, or decide when a hook should fire. There is no plugin registry, no configuration format, no lifecycle. If you want a package that scans a directory for plugins, this is not it.

For a single callback, Tapable is more machinery than the problem needs. You have to pick a hook class, name every argument, decide sync or async, and expose a hooks object. A plain function property does the same job with less surface area.

The ordering model has sharp edges too. Unknown names in before are ignored, so a plugin that expects to run before a tap that was renamed or removed will simply run somewhere else without an error. And the hook class is a contract: swapping a SyncHook for an AsyncSeriesHook later changes which tap methods are valid and whether callers get a return value, which is a breaking change for every plugin already written against it. The README does not document a migration path for that, so treat the hook class as part of your public API from day one.

Tapable against Node's EventEmitter

The obvious alternative in a Node codebase is the built-in EventEmitter. The difference is in the calling contract. EventEmitter is fire-and-forget: emit returns a boolean, listeners are synchronous, and there is no return value threading, no bail, no waterfall, and no ordering control beyond the order listeners were added. Tapable's hook classes exist precisely to cover the cases EventEmitter does not: a value passed through a chain of plugins, a first-responder-wins hook, a hook that runs its plugins in parallel and waits for all of them.

EventEmitter also has no equivalent of the before option or of withOptions, so a library cannot pre-configure ordering for the plugins it ships. On the other side, EventEmitter is in the standard library, needs no dependency, and any Node developer already knows it. If your extension points are notifications rather than transformations, and you do not care about ordering, EventEmitter costs you nothing and Tapable costs you a dependency plus a hook class decision per extension point.

Code generation, maintenance and the MIT licence

The README explains that a Hook compiles a method tailored to how it is used. The generated code depends on the number of registered plugins (none, one, many), the kind of plugins registered (sync, async, promise), the call method used, the number of arguments, and whether interception is used. The stated goal is "fastest possible execution". The practical consequence for readers is that the hook's declared argument list is not just documentation; it feeds the compiled runner, so changing it changes generated code.

The repository layout is conventional for a small library: lib for source, test, a benchmark directory, a TypeScript declaration file at the root, and a changesets directory. Releases are managed through changesets, with version and release scripts wired to changeset version and changeset publish. The last push to the default branch was on 2026-09-18, and the most recent release listed is v2.3.3 on 2026-04-21. The project is not archived.

The licence is MIT, declared in package.json and present as a LICENSE file at the repository root. For adopters that means the usual permissive terms: you can use, modify and redistribute it, subject to the licence text. The package also points to an OpenCollective funding page. None of this is legal advice; read the LICENSE file and your own organisation's policy.

Editorial conclusion

Adopt Tapable if you are writing a library or build tool that needs named, ordered extension points and you are willing to expose a hooks property as the public surface. Do not reach for it in application code with a single callback, where a plain function or an EventEmitter is less machinery. Before committing, read the hook classes table in the README and decide which of the nine classes matches your call pattern, since the choice fixes whether callers get a return value and whether plugins may be async. Then verify the licence file and the version you install, because the tap* methods differ between hook types.

Frequently asked questions

What is Tapable?

Tapable is a JavaScript module that exposes Hook classes for building plugin systems. The README describes it as a module whose Hook classes "can be used to create hooks for plugins", and it is published on npm as tapable under the MIT licence.

How do I install Tapable from npm?

The README gives one command, npm install --save tapable. The published package includes the lib directory and a tapable.d.ts type declaration, so no extra type package is needed.

What is the difference between tap, tapAsync and tapPromise in Tapable?

tap registers a synchronous callback, tapAsync registers a callback-based async callback whose last argument is a node-style callback, and tapPromise registers a promise-returning callback. Which of the three is valid depends on the hook class, and the README notes that a tapPromise callback returning a non-thenable causes the hook to throw.

How does Tapable decide the order in which plugins run?

Taps run in registration order by default. A tap registered with an options object can set stage, where lower stages run earlier, or before, which inserts it ahead of named taps; the README states that when both are used, before wins for the taps it targets.

Does Tapable return values from plugins?

Not from every hook class. The README warns that calling a SyncHook or AsyncParallelHook returns undefined even when a plugin returned a value, and says to use SyncWaterfallHook or AsyncSeriesWaterfallHook respectively when you need the value back.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. webpack/tapable on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/webpack-tapable.svg)](https://hysenlabs.com/projects/webpack-tapable)