source: frontend/node_modules/tapable/README.md

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 29.5 KB
RevLine 
[9af201e]1# Tapable
2
3The tapable package exposes many Hook classes, which can be used to create hooks for plugins.
4
5```javascript
6const {
7 AsyncParallelBailHook,
8 AsyncParallelHook,
9 AsyncSeriesBailHook,
10 AsyncSeriesHook,
11 AsyncSeriesWaterfallHook,
12 SyncBailHook,
13 SyncHook,
14 SyncLoopHook,
15 SyncWaterfallHook
16} = require("tapable");
17```
18
19## Installation
20
21```shell
22npm install --save tapable
23```
24
25## Usage
26
27All Hook constructors take one optional argument, which is a list of argument names as strings.
28
29```js
30const hook = new SyncHook(["arg1", "arg2", "arg3"]);
31```
32
33The best practice is to expose all hooks of a class in a `hooks` property:
34
35```js
36class Car {
37 constructor() {
38 this.hooks = {
39 accelerate: new SyncHook(["newSpeed"]),
40 brake: new SyncHook(),
41 calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
42 };
43 }
44
45 /* ... */
46}
47```
48
49Other people can now use these hooks:
50
51```js
52const myCar = new Car();
53
54// Use the tap method to add a consumer (plugin)
55myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
56```
57
58It's required to pass a name to identify the plugin/reason.
59
60You may receive arguments:
61
62```js
63myCar.hooks.accelerate.tap("LoggerPlugin", (newSpeed) =>
64 console.log(`Accelerating to ${newSpeed}`)
65);
66```
67
68For sync hooks, `tap` is the only valid method to add a plugin. Async hooks also support async plugins:
69
70```js
71myCar.hooks.calculateRoutes.tapPromise(
72 "GoogleMapsPlugin",
73 (source, target, routesList) =>
74 // return a promise
75 google.maps.findRoute(source, target).then((route) => {
76 routesList.add(route);
77 })
78);
79myCar.hooks.calculateRoutes.tapAsync(
80 "BingMapsPlugin",
81 (source, target, routesList, callback) => {
82 bing.findRoute(source, target, (err, route) => {
83 if (err) return callback(err);
84 routesList.add(route);
85 // call the callback
86 callback();
87 });
88 }
89);
90
91// You can still use sync plugins
92myCar.hooks.calculateRoutes.tap(
93 "CachedRoutesPlugin",
94 (source, target, routesList) => {
95 const cachedRoute = cache.get(source, target);
96 if (cachedRoute) routesList.add(cachedRoute);
97 }
98);
99```
100
101The class declaring these hooks needs to call them:
102
103```js
104class Car {
105 /**
106 * You won't get returned value from SyncHook or AsyncParallelHook,
107 * to do that, use SyncWaterfallHook and AsyncSeriesWaterfallHook respectively
108 */
109
110 setSpeed(newSpeed) {
111 // following call returns undefined even when you returned values
112 this.hooks.accelerate.call(newSpeed);
113 }
114
115 useNavigationSystemPromise(source, target) {
116 const routesList = new List();
117 return this.hooks.calculateRoutes
118 .promise(source, target, routesList)
119 .then((res) =>
120 // res is undefined for AsyncParallelHook
121 routesList.getRoutes()
122 );
123 }
124
125 useNavigationSystemAsync(source, target, callback) {
126 const routesList = new List();
127 this.hooks.calculateRoutes.callAsync(source, target, routesList, (err) => {
128 if (err) return callback(err);
129 callback(null, routesList.getRoutes());
130 });
131 }
132}
133```
134
135The Hook will compile a method with the most efficient way of running your plugins. It generates code depending on:
136
137- The number of registered plugins (none, one, many)
138- The kind of registered plugins (sync, async, promise)
139- The used call method (sync, async, promise)
140- The number of arguments
141- Whether interception is used
142
143This ensures fastest possible execution. See [Code generation](#code-generation) for more details on the runtime compilation.
144
145## Plugin API
146
147A plugin registers a callback on a hook using one of the `tap*` methods. The hook type determines which of these are valid (see [Hook classes](#hook-classes)):
148
149- `hook.tap(nameOrOptions, fn)` — register a synchronous callback.
150- `hook.tapAsync(nameOrOptions, fn)` — register a callback-based async callback. The last argument passed to `fn` is a node-style callback `(err, result)`.
151- `hook.tapPromise(nameOrOptions, fn)` — register a promise-returning async callback. If `fn` returns something that is not thenable, the hook throws.
152
153The first argument can be either a string (the plugin name) or an options object that also allows influencing the order in which taps run:
154
155```js
156hook.tap(
157 {
158 name: "MyPlugin",
159 stage: -10, // lower stages run earlier, default is 0
160 before: "OtherPlugin" // run before a named tap (string or string[])
161 },
162 (...args) => {
163 /* ... */
164 }
165);
166```
167
168| Option | Type | Description |
169| -------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
170| `name` | `string` | Required. Identifies the tap for debugging, interceptors, and the `before` option. |
171| `stage` | `number` | Defaults to `0`. Taps with a lower stage run before taps with a higher stage. Taps with the same stage run in registration order. |
172| `before` | `string` \| `string[]` | The tap is inserted before the named tap(s). Unknown names are ignored. Combined with `stage`, `before` wins for the taps it targets; other taps are still ordered by `stage`. |
173
174The `name` is also used by some ecosystems (like webpack) for profiling and error messages. Within a single tap registration, later interceptors' `register` hooks may still replace the tap object (see [Interception](#interception)).
175
176### `hook.withOptions(options)`
177
178`withOptions` returns a facade around the hook whose `tap*` methods automatically merge `options` into every registration. It is useful for libraries that want to pre-configure a `stage` or `before` for all the taps they add:
179
180```js
181const lateHook = myCar.hooks.accelerate.withOptions({ stage: 10 });
182lateHook.tap("LogAfterOthers", (speed) => console.log("final speed", speed));
183// equivalent to: myCar.hooks.accelerate.tap({ name: "LogAfterOthers", stage: 10 }, ...)
184```
185
186The returned object does not expose the `call*` methods, so it is safe to hand out to plugins.
187
188A runnable example showing how `withOptions` influences tap ordering:
189
190```js
191const { SyncHook } = require("tapable");
192
193const hook = new SyncHook(["value"]);
194
195hook.tap("Default", (v) => console.log("default", v));
196
197// Pre-configure stage: 10 so all taps registered through `late` run last.
198const late = hook.withOptions({ stage: 10 });
199late.tap("RunLast", (v) => console.log("last", v));
200
201// Pre-configure stage: -10 so these taps run first. Each facade can also
202// be further narrowed via `withOptions`.
203const early = hook.withOptions({ stage: -10 });
204early.tap("RunFirst", (v) => console.log("first", v));
205
206hook.call(1);
207// first 1
208// default 1
209// last 1
210```
211
212Per-tap options override values from `withOptions`. For example, `late.tap({ name: "Override", stage: 0 }, fn)` ignores the facade's `stage: 10` and registers `fn` at stage `0`.
213
214## Hook types
215
216Each hook can be tapped with one or several functions. How they are executed depends on the hook type:
217
218- Basic hook (without “Waterfall”, “Bail” or “Loop” in its name). This hook simply calls every function it tapped in a row.
219
220- **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.
221
222- **Bail**. A bail hook allows exiting early. When any of the tapped function returns anything, the bail hook will stop executing the remaining ones.
223
224- **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.
225
226Additionally, hooks can be synchronous or asynchronous. To reflect this, there’re “Sync”, “AsyncSeries”, and “AsyncParallel” hook classes:
227
228- **Sync**. A sync hook can only be tapped with synchronous functions (using `myHook.tap()`).
229
230- **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.
231
232- **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.
233
234The 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.
235
236## Hook classes
237
238The table below summarizes the 9 built-in hook classes. For each class:
239
240- **Tap methods** are the `tapX` variants that may be used to register a handler.
241- **Call methods** are the ways the owner of the hook can trigger it.
242- **Result** is the value returned from `call` (or passed to the `callAsync` callback / resolved from the `promise` call).
243- **Returned value from tap** describes whether the value returned from a tapped function has an effect.
244
245| Class | Tap methods | Call methods | Result | Returned value from tap |
246| -------------------------- | ------------------------------- | ---------------------- | ----------------------------------------------- | ---------------------------------------------------- |
247| `SyncHook` | `tap` | `call` | `undefined` | ignored |
248| `SyncBailHook` | `tap` | `call` | first non-`undefined` value, or `undefined` | short-circuits the hook |
249| `SyncWaterfallHook` | `tap` | `call` | final value (first argument after the last tap) | passed as first argument to the next tap |
250| `SyncLoopHook` | `tap` | `call` | `undefined` | non-`undefined` restarts the loop from the first tap |
251| `AsyncParallelHook` | `tap`, `tapAsync`, `tapPromise` | `callAsync`, `promise` | `undefined` | ignored |
252| `AsyncParallelBailHook` | `tap`, `tapAsync`, `tapPromise` | `callAsync`, `promise` | first non-`undefined` value, or `undefined` | short-circuits the hook |
253| `AsyncSeriesHook` | `tap`, `tapAsync`, `tapPromise` | `callAsync`, `promise` | `undefined` | ignored |
254| `AsyncSeriesBailHook` | `tap`, `tapAsync`, `tapPromise` | `callAsync`, `promise` | first non-`undefined` value, or `undefined` | short-circuits the hook |
255| `AsyncSeriesLoopHook` | `tap`, `tapAsync`, `tapPromise` | `callAsync`, `promise` | `undefined` | non-`undefined` restarts the loop from the first tap |
256| `AsyncSeriesWaterfallHook` | `tap`, `tapAsync`, `tapPromise` | `callAsync`, `promise` | final value (first argument after the last tap) | passed as first argument to the next tap |
257
258Detailed behavior of each class:
259
260### SyncHook
261
262A basic synchronous hook. Every tapped function is called in registration order with the arguments passed to `call`. Return values from tapped functions are ignored and `call` returns `undefined`.
263
264- Tap methods: `tap`
265- Call methods: `call`
266- `tapAsync` and `tapPromise` throw an error.
267
268```js
269const hook = new SyncHook(["name"]);
270hook.tap("A", (name) => console.log(`hello ${name}`));
271hook.tap("B", (name) => console.log(`hi ${name}`));
272hook.call("world");
273// hello world
274// hi world
275```
276
277### SyncBailHook
278
279A synchronous hook that allows exiting early. Every tapped function is called in order until one returns a non-`undefined` value; that value becomes the result of `call` and the remaining taps are skipped. If all taps return `undefined`, `call` returns `undefined`.
280
281- Tap methods: `tap`
282- Call methods: `call`
283
284```js
285const hook = new SyncBailHook(["value"]);
286hook.tap("Negative", (v) => (v < 0 ? "negative" : undefined));
287hook.tap("Zero", (v) => (v === 0 ? "zero" : undefined));
288hook.tap("Positive", (v) => "positive");
289
290hook.call(-1); // "negative" (later taps skipped)
291hook.call(5); // "positive"
292```
293
294### SyncWaterfallHook
295
296A synchronous hook that threads a value through its tapped functions. The first argument passed to `call` is forwarded to the first tap. If a tap returns a non-`undefined` value it replaces that argument for the next tap; otherwise the previous value is kept. `call` returns the value after the last tap has run. Additional arguments (if any) are passed through unchanged.
297
298- Tap methods: `tap`
299- Call methods: `call`
300
301```js
302const hook = new SyncWaterfallHook(["value"]);
303hook.tap("Double", (v) => v * 2);
304hook.tap("PlusOne", (v) => v + 1);
305
306hook.call(3); // 7 -> (3 * 2) + 1
307```
308
309### SyncLoopHook
310
311A synchronous hook that keeps re-running its taps until all of them return `undefined` for a full pass. Whenever a tap returns a non-`undefined` value the hook restarts from the first tap. `call` returns `undefined`.
312
313- Tap methods: `tap`
314- Call methods: `call`
315
316```js
317const hook = new SyncLoopHook(["state"]);
318let retries = 3;
319hook.tap("Retry", () => {
320 if (retries-- > 0) return true; // non-undefined restarts the loop
321});
322hook.tap("Log", () => console.log("pass"));
323
324hook.call({});
325// pass (runs once all taps return undefined)
326```
327
328### AsyncParallelHook
329
330An asynchronous hook that runs all of its tapped functions in parallel. It completes when every tap has signalled completion (sync return, callback, or promise resolution). Return values and resolution values are ignored; `callAsync`'s callback is invoked with no result and `promise()` resolves to `undefined`. If any tap errors, the error is forwarded and remaining taps still complete but their results are discarded.
331
332- Tap methods: `tap`, `tapAsync`, `tapPromise`
333- Call methods: `callAsync`, `promise`
334
335```js
336const hook = new AsyncParallelHook(["source"]);
337hook.tapPromise("Fetch", (src) => fetch(src));
338hook.tapAsync("Log", (src, cb) => {
339 console.log("fetching", src);
340 cb();
341});
342
343await hook.promise("https://example.com");
344```
345
346### AsyncParallelBailHook
347
348Like `AsyncParallelHook`, but designed to bail out with a result. All tapped functions start in parallel; the first tap to produce a non-`undefined` value (synchronously, via its callback, or by resolving its promise) determines the hook’s result. The remaining taps continue to run but their results are ignored. Order is determined by tap registration order: an earlier tap’s value takes precedence over a later one’s, even if the later one finishes first.
349
350- Tap methods: `tap`, `tapAsync`, `tapPromise`
351- Call methods: `callAsync`, `promise`
352
353```js
354const hook = new AsyncParallelBailHook(["key"]);
355hook.tapPromise("Cache", async (key) => cache.get(key));
356hook.tapPromise("Db", async (key) => db.lookup(key));
357
358const value = await hook.promise("user:42");
359// First non-undefined result (by registration order) wins.
360```
361
362### AsyncSeriesHook
363
364An asynchronous hook that runs tapped functions one after another, waiting for each to finish before starting the next. Results are ignored; `callAsync`'s callback is invoked with no result and `promise()` resolves to `undefined`. The first error aborts the series.
365
366- Tap methods: `tap`, `tapAsync`, `tapPromise`
367- Call methods: `callAsync`, `promise`
368
369```js
370const hook = new AsyncSeriesHook(["request"]);
371hook.tapPromise("Authenticate", async (req) => authenticate(req));
372hook.tapPromise("Log", async (req) => logger.info(req.url));
373
374await hook.promise(request);
375```
376
377### AsyncSeriesBailHook
378
379An asynchronous series hook that allows exiting early. Tapped functions run one after another; as soon as one produces a non-`undefined` value, that value becomes the hook’s result and the remaining taps are skipped.
380
381- Tap methods: `tap`, `tapAsync`, `tapPromise`
382- Call methods: `callAsync`, `promise`
383
384```js
385const hook = new AsyncSeriesBailHook(["id"]);
386hook.tapPromise("Memory", async (id) => memory.get(id));
387hook.tapPromise("Disk", async (id) => disk.read(id));
388
389const value = await hook.promise("doc-1");
390// Stops at the first tap that produces a value.
391```
392
393### AsyncSeriesLoopHook
394
395An asynchronous series hook that loops. Tapped functions run one after another; whenever a tap produces a non-`undefined` value the hook restarts from the first tap. The hook completes once a full pass yields `undefined` from every tap. The result is always `undefined`.
396
397- Tap methods: `tap`, `tapAsync`, `tapPromise`
398- Call methods: `callAsync`, `promise`
399
400```js
401const hook = new AsyncSeriesLoopHook(["job"]);
402hook.tapPromise("Process", async (job) => {
403 const more = await job.step();
404 if (more) return true; // restart the loop
405});
406
407await hook.promise(job);
408```
409
410### AsyncSeriesWaterfallHook
411
412An asynchronous series hook that threads a value through its taps. The first argument passed to `callAsync` / `promise` is forwarded to the first tap. A tap's non-`undefined` return / callback / resolution value replaces it for the next tap; `undefined` keeps the previous value. The hook completes with the value after the last tap.
413
414- Tap methods: `tap`, `tapAsync`, `tapPromise`
415- Call methods: `callAsync`, `promise`
416
417```js
418const hook = new AsyncSeriesWaterfallHook(["source"]);
419hook.tapPromise("Read", async (src) => fs.readFile(src, "utf8"));
420hook.tapPromise("Trim", async (text) => text.trim());
421
422const output = await hook.promise("./input.txt");
423```
424
425## Interception
426
427All hooks expose an `intercept(interceptor)` method. An interceptor is a plain object whose methods are invoked at specific points during the lifetime of the hook. Interceptors are invoked in registration order before the taps, and are useful for logging, tracing, profiling, or re-mapping tap options.
428
429```js
430myCar.hooks.calculateRoutes.intercept({
431 name: "LoggingInterceptor",
432 call: (source, target, routesList) => {
433 console.log("Starting to calculate routes");
434 },
435 tap: (tapInfo) => {
436 // tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ..., stage: 0 }
437 console.log(`${tapInfo.name} is running`);
438 },
439 register: (tapInfo) => {
440 // Called once per tap (and for each tap already registered when the
441 // interceptor is added). Return a new tapInfo object to replace it.
442 console.log(`${tapInfo.name} is registered`);
443
444 return tapInfo;
445 }
446});
447```
448
449| Handler | Signature | When it runs |
450| ---------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
451| `call` | `(...args) => void` | Before the hook starts executing its taps. Receives the arguments passed to `call` / `callAsync` / `promise`. |
452| `tap` | `(tap: Tap) => void` | Before each tap runs. The `tap` object is a snapshot — mutations are ignored. |
453| `loop` | `(...args) => void` | At the start of each iteration of a `SyncLoopHook` / `AsyncSeriesLoopHook`. |
454| `error` | `(err: Error) => void` | Whenever a tap throws, rejects, or calls its callback with an error. |
455| `result` | `(result: any) => void` | When a bail or waterfall hook produces a value, or when a tap produces one for a loop hook. |
456| `done` | `() => void` | When the hook finishes successfully (no error, no early bail). |
457| `register` | `(tap: Tap) => Tap \| undefined` | Once per tap at registration time (including taps that existed before the interceptor was added). Return a new `Tap` object to replace it. |
458| `name` | `string` | Optional label used by ecosystems for debugging. |
459| `context` | `boolean` | Opt into the shared `context` object. See [Context](#context). |
460
461Adding an interceptor invalidates the hook's compiled call function — the next `call` / `callAsync` / `promise` recompiles it so that the new interceptor is woven in.
462
463## Context
464
465Plugins and interceptors can opt-in to access an optional `context` object, which can be used to pass arbitrary values to subsequent plugins and interceptors.
466
467```js
468myCar.hooks.accelerate.intercept({
469 context: true,
470 tap: (context, tapInfo) => {
471 // tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
472 console.log(`${tapInfo.name} is doing it's job`);
473
474 // `context` starts as an empty object if at least one plugin uses `context: true`.
475 // If no plugins use `context: true`, then `context` is undefined.
476 if (context) {
477 // Arbitrary properties can be added to `context`, which plugins can then access.
478 context.hasMuffler = true;
479 }
480 }
481});
482
483myCar.hooks.accelerate.tap(
484 {
485 name: "NoisePlugin",
486 context: true
487 },
488 (context, newSpeed) => {
489 if (context && context.hasMuffler) {
490 console.log("Silence...");
491 } else {
492 console.log("Vroom!");
493 }
494 }
495);
496```
497
498## HookMap
499
500A `HookMap` is a helper class that lazily creates hooks per key. The constructor takes a factory function; the first time a key is requested via `for(key)`, the factory is called and the resulting hook is cached.
501
502```js
503const keyedHook = new HookMap((key) => new SyncHook(["arg"]));
504```
505
506Plugins use `for(key)` to obtain the hook for a specific key (creating it on demand) and then `tap` on it as usual:
507
508```js
509keyedHook.for("some-key").tap("MyPlugin", (arg) => {
510 /* ... */
511});
512keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => {
513 /* ... */
514});
515keyedHook.for("some-key").tapPromise("MyPlugin", (arg) => {
516 /* ... */
517});
518```
519
520The owner of the `HookMap` uses `get(key)` to look up an existing hook without creating one. This is typically preferred on the calling side so that keys no plugin cares about are never materialized:
521
522```js
523const hook = keyedHook.get("some-key");
524if (hook !== undefined) {
525 hook.callAsync("arg", (err) => {
526 /* ... */
527 });
528}
529```
530
531A `HookMap` can also be intercepted. `intercept({ factory })` wraps the factory so you can customize or replace the hook returned for each new key.
532
533## Code generation
534
535Tapable does not iterate over taps at call time. Instead, the first time `call`, `callAsync` or `promise` is invoked after the hook has been modified, the hook compiles a specialized function using `new Function(...)` and caches it on the instance. This is what the README means by "evals in code": the hook's dispatch logic is generated as a string and turned into a real JavaScript function the engine can inline and optimize.
536
537The generated function is tailored to:
538
539- **Call type** — whether the owner called `call` (sync), `callAsync` (callback), or `promise`. Each produces a different skeleton — e.g. `promise()` wraps the body in `new Promise((_resolve, _reject) => { ... })`.
540- **Tap types** — for each tap, the generator emits the right invocation pattern: direct call for `tap`, node-style callback wrapping for `tapAsync`, and `.then(...)` chaining for `tapPromise`.
541- **Hook class** — `SyncHook` emits a straight-line sequence of calls; `SyncBailHook` emits early-return checks; `SyncWaterfallHook` threads a value through calls; loop hooks wrap the body in a re-entry loop; `AsyncParallel*` fans the taps out and counts completions; `AsyncSeries*` chains them.
542- **Interceptors** — if interceptors are attached, calls to their `call`/`tap`/`loop`/`error`/`result`/`done` handlers are spliced into the generated body; otherwise they cost nothing.
543- **Context** — a `_context` object is only created when at least one tap or interceptor opts into it with `context: true`.
544- **Arity** — the generated code hard-codes the number of arguments declared when the hook was constructed, so no `arguments`/rest handling happens at runtime.
545
546The compiled function is invalidated (reset back to a one-shot "recompile then call" trampoline) whenever the hook's shape changes — i.e. on any new `tap*` or `intercept` call. Steady-state calls therefore run straight through the cached function with no per-tap branching.
547
548### Why this matters
549
550- You only pay for features you use. An interceptor-free, sync-only hook compiles down to a short sequence of direct function calls.
551- Debugging a hook means reading the generated source. If you need to see it, `Hook.prototype.compile` returns the `new Function(...)` result — log `hook._createCall("sync").toString()` (or `"async"` / `"promise"`) to inspect the body.
552- Because the dispatch is code-generated, a hook's behavior is fully determined at compile time. Mutating tap options after registration (for example, changing `stage` on an existing `Tap` object) will not reorder taps until you cause a recompile.
553
554## Hook/HookMap interface
555
556Public (callable by anyone holding a reference to the hook, i.e. the plugins):
557
558```ts
559interface Hook {
560 tap: (name: string | Tap, fn: (context?, ...args) => Result) => void;
561 tapAsync: (
562 name: string | Tap,
563 fn: (
564 context?,
565 ...args,
566 callback: (err: Error | null, result: Result) => void
567 ) => void
568 ) => void;
569 tapPromise: (
570 name: string | Tap,
571 fn: (context?, ...args) => Promise<Result>
572 ) => void;
573 intercept: (interceptor: HookInterceptor) => void;
574 withOptions: (
575 options: TapOptions
576 ) => Omit<Hook, "call" | "callAsync" | "promise">;
577}
578
579interface HookInterceptor {
580 name?: string;
581 call?: (context?, ...args) => void;
582 loop?: (context?, ...args) => void;
583 tap?: (context?, tap: Tap) => void;
584 error?: (err: Error) => void;
585 result?: (result: any) => void;
586 done?: () => void;
587 register?: (tap: Tap) => Tap | undefined;
588 context?: boolean;
589}
590
591interface HookMap {
592 for: (key: any) => Hook;
593 intercept: (interceptor: HookMapInterceptor) => void;
594}
595
596interface HookMapInterceptor {
597 factory: (key: any, hook: Hook) => Hook;
598}
599
600interface Tap {
601 name: string;
602 type: "sync" | "async" | "promise";
603 fn: Function;
604 stage: number;
605 context: boolean;
606 before?: string | Array<string>;
607}
608```
609
610Protected (only for the class containing the hook — it owns the right to trigger it):
611
612```ts
613interface Hook {
614 isUsed: () => boolean;
615 call: (...args) => Result;
616 promise: (...args) => Promise<Result>;
617 callAsync: (
618 ...args,
619 callback: (err: Error | null, result: Result) => void
620 ) => void;
621}
622
623interface HookMap {
624 get: (key: any) => Hook | undefined;
625 for: (key: any) => Hook;
626}
627```
628
629`isUsed()` returns `true` when the hook has at least one tap or interceptor registered. Hook owners can use it to skip expensive argument preparation when no plugin is listening:
630
631```js
632class Car {
633 // ...
634 setSpeed(newSpeed) {
635 if (this.hooks.accelerate.isUsed()) {
636 this.hooks.accelerate.call(newSpeed);
637 }
638 }
639 // ...
640}
641```
642
643## MultiHook
644
645A `MultiHook` is a Hook-like facade that forwards `tap`, `tapAsync`, `tapPromise`, `intercept`, and `withOptions` to several underlying hooks at once. It does not expose `call*` methods — only the owners of the wrapped hooks decide when each of them runs. It is the typical way a class exposes a "happens on any of these events" listening surface without having the plugin wire itself up to every hook individually.
646
647### Fan out a tap to several hooks
648
649```js
650const { MultiHook, SyncHook } = require("tapable");
651
652class Car {
653 constructor() {
654 const accelerate = new SyncHook(["newSpeed"]);
655 const brake = new SyncHook();
656 this.hooks = {
657 accelerate,
658 brake,
659 // `anyMovement` is not a real hook — it simply re-registers taps
660 // on both `accelerate` and `brake`.
661 anyMovement: new MultiHook([accelerate, brake])
662 };
663 }
664}
665
666const car = new Car();
667car.hooks.anyMovement.tap("Telemetry", () => console.log("car moved"));
668
669car.hooks.accelerate.call(42); // "car moved"
670car.hooks.brake.call(); // "car moved"
671```
672
673The `MultiHook` has no state of its own: the tap above ends up inside `accelerate.taps` and `brake.taps`.
674
675### Forwarding async taps
676
677`tapAsync` / `tapPromise` forward to every wrapped hook — it is the plugin's job to make sure they are all compatible. Registering a `tapPromise` on a `MultiHook` that wraps a `SyncHook` will throw at registration time for that hook.
678
679```js
680const build = new AsyncSeriesHook(["stats"]);
681const rebuild = new AsyncSeriesHook(["stats"]);
682const anyBuild = new MultiHook([build, rebuild]);
683
684anyBuild.tapPromise("Report", async (stats) => report.send(stats));
685```
686
687### Shared interceptors and options
688
689`intercept` and `withOptions` are also forwarded, so a `MultiHook` can be used to attach the same interceptor or pre-configured options to a group of hooks:
690
691```js
692const anyBuild = new MultiHook([build, rebuild]);
693
694anyBuild.intercept({
695 call: () => console.log("build started"),
696 done: () => console.log("build done")
697});
698
699// Every tap added through `late` is staged late on both underlying hooks.
700const late = anyBuild.withOptions({ stage: 10 });
701late.tap("RunLast", () => {
702 /* ... */
703});
704```
705
706### `isUsed`
707
708`isUsed()` returns `true` if any of the wrapped hooks has at least one tap or interceptor, which lets the owner cheaply skip work when no one is listening on any of them:
709
710```js
711if (this.hooks.anyMovement.isUsed()) {
712 // expensive telemetry payload is only built when a plugin actually cares
713 this.hooks.accelerate.call(computeSpeed());
714}
715```
Note: See TracBrowser for help on using the repository browser.