3 The tapable package expose many Hook classes, which can be used to create hooks for plugins.
12 AsyncParallelBailHook,
15 AsyncSeriesWaterfallHook
16 } = require("tapable");
22 npm install --save tapable
27 All Hook constructors take one optional argument, which is a list of argument names as strings.
30 const hook = new SyncHook(["arg1", "arg2", "arg3"]);
33 The best practice is to expose all hooks of a class in a `hooks` property:
39 accelerate: new SyncHook(["newSpeed"]),
40 brake: new SyncHook(),
41 calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
49 Other people can now use these hooks:
52 const myCar = new Car();
54 // Use the tap method to add a consument
55 myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
58 It's required to pass a name to identify the plugin/reason.
60 You may receive arguments:
63 myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
66 For sync hooks, `tap` is the only valid method to add a plugin. Async hooks also support async plugins:
69 myCar.hooks.calculateRoutes.tapPromise("GoogleMapsPlugin", (source, target, routesList) => {
71 return google.maps.findRoute(source, target).then(route => {
72 routesList.add(route);
75 myCar.hooks.calculateRoutes.tapAsync("BingMapsPlugin", (source, target, routesList, callback) => {
76 bing.findRoute(source, target, (err, route) => {
77 if(err) return callback(err);
78 routesList.add(route);
84 // You can still use sync plugins
85 myCar.hooks.calculateRoutes.tap("CachedRoutesPlugin", (source, target, routesList) => {
86 const cachedRoute = cache.get(source, target);
88 routesList.add(cachedRoute);
91 The class declaring these hooks need to call them:
96 * You won't get returned value from SyncHook or AsyncParallelHook,
97 * to do that, use SyncWaterfallHook and AsyncSeriesWaterfallHook respectively
101 // following call returns undefined even when you returned values
102 this.hooks.accelerate.call(newSpeed);
105 useNavigationSystemPromise(source, target) {
106 const routesList = new List();
107 return this.hooks.calculateRoutes.promise(source, target, routesList).then((res) => {
108 // res is undefined for AsyncParallelHook
109 return routesList.getRoutes();
113 useNavigationSystemAsync(source, target, callback) {
114 const routesList = new List();
115 this.hooks.calculateRoutes.callAsync(source, target, routesList, err => {
116 if(err) return callback(err);
117 callback(null, routesList.getRoutes());
123 The Hook will compile a method with the most efficient way of running your plugins. It generates code depending on:
124 * The number of registered plugins (none, one, many)
125 * The kind of registered plugins (sync, async, promise)
126 * The used call method (sync, async, promise)
127 * The number of arguments
128 * Whether interception is used
130 This ensures fastest possible execution.
134 Each hook can be tapped with one or several functions. How they are executed depends on the hook type:
136 * Basic hook (without “Waterfall”, “Bail” or “Loop” in its name). This hook simply calls every function it tapped in a row.
138 * __Waterfall__. A waterfall hook also calls each tapped function in a row. Unlike the basic hook, it passes a return value from each function to the next function.
140 * __Bail__. A bail hook allows exiting early. When any of the tapped function returns anything, the bail hook will stop executing the remaining ones.
142 * __Loop__. When a plugin in a loop hook returns a non-undefined value the hook will restart from the first plugin. It will loop until all plugins return undefined.
144 Additionally, hooks can be synchronous or asynchronous. To reflect this, there’re “Sync”, “AsyncSeries”, and “AsyncParallel” hook classes:
146 * __Sync__. A sync hook can only be tapped with synchronous functions (using `myHook.tap()`).
148 * __AsyncSeries__. An async-series hook can be tapped with synchronous, callback-based and promise-based functions (using `myHook.tap()`, `myHook.tapAsync()` and `myHook.tapPromise()`). They call each async method in a row.
150 * __AsyncParallel__. An async-parallel hook can also be tapped with synchronous, callback-based and promise-based functions (using `myHook.tap()`, `myHook.tapAsync()` and `myHook.tapPromise()`). However, they run each async method in parallel.
152 The hook type is reflected in its class name. E.g., `AsyncSeriesWaterfallHook` allows asynchronous functions and runs them in series, passing each function’s return value into the next function.
157 All Hooks offer an additional interception API:
160 myCar.hooks.calculateRoutes.intercept({
161 call: (source, target, routesList) => {
162 console.log("Starting to calculate routes");
164 register: (tapInfo) => {
165 // tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
166 console.log(`${tapInfo.name} is doing its job`);
167 return tapInfo; // may return a new tapInfo object
172 **call**: `(...args) => void` Adding `call` to your interceptor will trigger when hooks are triggered. You have access to the hooks arguments.
174 **tap**: `(tap: Tap) => void` Adding `tap` to your interceptor will trigger when a plugin taps into a hook. Provided is the `Tap` object. `Tap` object can't be changed.
176 **loop**: `(...args) => void` Adding `loop` to your interceptor will trigger for each loop of a looping hook.
178 **register**: `(tap: Tap) => Tap | undefined` Adding `register` to your interceptor will trigger for each added `Tap` and allows to modify it.
182 Plugins and interceptors can opt-in to access an optional `context` object, which can be used to pass arbitrary values to subsequent plugins and interceptors.
185 myCar.hooks.accelerate.intercept({
187 tap: (context, tapInfo) => {
188 // tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
189 console.log(`${tapInfo.name} is doing it's job`);
191 // `context` starts as an empty object if at least one plugin uses `context: true`.
192 // If no plugins use `context: true`, then `context` is undefined.
194 // Arbitrary properties can be added to `context`, which plugins can then access.
195 context.hasMuffler = true;
200 myCar.hooks.accelerate.tap({
203 }, (context, newSpeed) => {
204 if (context && context.hasMuffler) {
205 console.log("Silence...");
207 console.log("Vroom!");
214 A HookMap is a helper class for a Map with Hooks
217 const keyedHook = new HookMap(key => new SyncHook(["arg"]))
221 keyedHook.for("some-key").tap("MyPlugin", (arg) => { /* ... */ });
222 keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => { /* ... */ });
223 keyedHook.for("some-key").tapPromise("MyPlugin", (arg) => { /* ... */ });
227 const hook = keyedHook.get("some-key");
228 if(hook !== undefined) {
229 hook.callAsync("arg", err => { /* ... */ });
233 ## Hook/HookMap interface
239 tap: (name: string | Tap, fn: (context?, ...args) => Result) => void,
240 tapAsync: (name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void,
241 tapPromise: (name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void,
242 intercept: (interceptor: HookInterceptor) => void
245 interface HookInterceptor {
246 call: (context?, ...args) => void,
247 loop: (context?, ...args) => void,
248 tap: (context?, tap: Tap) => void,
249 register: (tap: Tap) => Tap,
254 for: (key: any) => Hook,
255 intercept: (interceptor: HookMapInterceptor) => void
258 interface HookMapInterceptor {
259 factory: (key: any, hook: Hook) => Hook
268 before?: string | Array
272 Protected (only for the class containing the hook):
276 isUsed: () => boolean,
277 call: (...args) => Result,
278 promise: (...args) => Promise<Result>,
279 callAsync: (...args, callback: (err, result: Result) => void) => void,
283 get: (key: any) => Hook | undefined,
284 for: (key: any) => Hook
290 A helper Hook-like class to redirect taps to multiple other hooks:
293 const { MultiHook } = require("tapable");
295 this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);