hookable 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Pooya Parsa <pooya@pi0.io>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,271 @@
1
+ # Hookable
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![packagephobia][packagephobia-src]][packagephobia-href]
6
+ [![Github Actions CI][github-actions-ci-src]][github-actions-ci-href]
7
+ [![Codecov][codecov-src]][codecov-href]
8
+
9
+ > Awaitable hook system
10
+
11
+ ## Install
12
+
13
+ Using yarn:
14
+
15
+ ```bash
16
+ yarn add hookable
17
+ ```
18
+
19
+ Using npm:
20
+
21
+ ```bash
22
+ npm install hookable
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ **Method A: Create a hookable instance:**
28
+
29
+ ```js
30
+ import { createHooks } from 'hookable'
31
+
32
+ // Create a hookable instance
33
+ const hooks = createHooks()
34
+
35
+ // Hook on 'hello'
36
+ hooks.hook('hello', () => { console.log('Hello World' )})
37
+
38
+ // Call 'hello' hook
39
+ hooks.callHook('hello')
40
+ ```
41
+
42
+ **Method B: Extend your base class from Hookable:**
43
+
44
+ ```js
45
+ import { Hookable } from 'hookable'
46
+
47
+ export default class FooLib extends Hookable {
48
+ constructor() {
49
+ // Call to parent to initialize
50
+ super()
51
+ // Initialize Hookable with custom logger
52
+ // super(consola)
53
+ }
54
+
55
+ async someFunction() {
56
+ // Call and wait for `hook1` hooks (if any) sequential
57
+ await this.callHook('hook1')
58
+ }
59
+ }
60
+ ```
61
+
62
+ **Inside plugins, register for any hook:**
63
+
64
+ ```js
65
+ const lib = new FooLib()
66
+
67
+ // Register a handler for `hook2`
68
+ lib.hook('hook2', async () => { /* ... */ })
69
+
70
+ // Register multiply handlers at once
71
+ lib.addHooks({
72
+ hook1: async () => { /* ... */ },
73
+ hook2: [ /* can be also an array */ ]
74
+ })
75
+ ```
76
+
77
+ **Unregistering hooks:**
78
+
79
+ ```js
80
+ const lib = new FooLib()
81
+
82
+ const hook0 = async () => { /* ... */ }
83
+ const hook1 = async () => { /* ... */ }
84
+ const hook2 = async () => { /* ... */ }
85
+
86
+ // The hook() method returns an "unregister" function
87
+ const unregisterHook0 = lib.hook('hook0', hook0)
88
+ const unregisterHooks1and2 = lib.addHooks({ hook1, hook2 })
89
+
90
+ /* ... */
91
+
92
+ unregisterHook0()
93
+ unregisterHooks1and2()
94
+
95
+ // or
96
+
97
+ lib.removeHooks({ hook0, hook1 })
98
+ lib.removeHook('hook2', hook2)
99
+ ```
100
+
101
+ **Triggering a hook handler once:**
102
+
103
+ ```js
104
+ const lib = new FooLib()
105
+
106
+ const unregister = lib.hook('hook0', async () => {
107
+ // Unregister as soon as the hook is executed
108
+ unregister()
109
+
110
+ /* ... */
111
+ })
112
+ ```
113
+
114
+
115
+ ## Hookable class
116
+
117
+ ### `constructor()`
118
+
119
+ ### `hook (name, fn)`
120
+
121
+ Register a handler for a specific hook. `fn` must be a function.
122
+
123
+ Returns an `unregister` function that, when called, will remove the registered handler.
124
+
125
+ ### `hookOnce (name, fn)`
126
+
127
+ Similar to `hook` but unregisters hook once called.
128
+
129
+ Returns an `unregister` function that, when called, will remove the registered handler before first call.
130
+
131
+ ### `addHooks(configHooks)`
132
+
133
+ Flatten and register hooks object.
134
+
135
+ Example:
136
+
137
+ ```js
138
+ hookable.addHooks({
139
+ test: {
140
+ before: () => {},
141
+ after: () => {}
142
+ }
143
+ })
144
+
145
+ ```
146
+
147
+ This registers `test:before` and `test:after` hooks at bulk.
148
+
149
+ Returns an `unregister` function that, when called, will remove all the registered handlers.
150
+
151
+ ### `async callHook (name, ...args)`
152
+
153
+ Used by class itself to **sequentially** call handlers of a specific hook.
154
+
155
+ ### `callHookWith (name, callerFn)`
156
+
157
+ If you need custom control over how hooks are called, you can provide a custom function that will receive an array of handlers of a specific hook.
158
+
159
+ `callerFn` if a callback function that accepts two arguments, `hooks` and `args`:
160
+ - `hooks`: Array of user hooks to be called
161
+ - `args`: Array of arguments that should be passed each time calling a hook
162
+
163
+ ### `deprecateHook (old, name)`
164
+
165
+ Deprecate hook called `old` in favor of `name` hook.
166
+
167
+ ### `deprecateHooks (deprecatedHooks)`
168
+
169
+ Deprecate all hooks from an object (keys are old and values or newer ones).
170
+
171
+ ### `removeHook (name, fn)`
172
+
173
+ Remove a particular hook handler, if the `fn` handler is present.
174
+
175
+ ### `removeHooks (configHooks)`
176
+
177
+ Remove multiple hook handlers.
178
+
179
+ Example:
180
+
181
+ ```js
182
+ const handler = async () => { /* ... */ }
183
+
184
+ hookable.hook('test:before', handler)
185
+ hookable.addHooks({ test: { after: handler } })
186
+
187
+ // ...
188
+
189
+ hookable.removeHooks({
190
+ test: {
191
+ before: handler,
192
+ after: handler
193
+ }
194
+ })
195
+ ```
196
+
197
+ ### `beforeEach (syncCallback)`
198
+
199
+ Registers a (sync) callback to be called before each hook is being called.
200
+
201
+ ```js
202
+ hookable.beforeEach((event) => { console.log(`${event.name} hook is being called with ${event.args}`)})
203
+ hookable.hook('test', () => { console.log('running test hook') })
204
+
205
+ // test hook is being called with []
206
+ // running test hook
207
+ await hookable.callHook('test')
208
+ ```
209
+
210
+ ### `afterEach (syncCallback)`
211
+
212
+ Registers a (sync) callback to be called after each hook is being called.
213
+
214
+ ```js
215
+ hookable.afterEach((event) => { console.log(`${event.name} hook called with ${event.args}`)})
216
+ hookable.hook('test', () => { console.log('running test hook') })
217
+
218
+ // running test hook
219
+ // test hook called with []
220
+ await hookable.callHook('test')
221
+ ```
222
+
223
+ ### `createDebugger`
224
+
225
+ Automatically logs each hook that is called and how long it takes to run.
226
+
227
+ ```js
228
+ const debug = hookable.createDebugger(hooks, { tag: 'something' })
229
+
230
+ hooks.callHook('some-hook', 'some-arg')
231
+ // [something] some-hook: 0.21ms
232
+
233
+ debug.close()
234
+ ```
235
+
236
+ ## Migration
237
+
238
+ ### From `4.x` to `5.x`
239
+
240
+ - Type checking improved. You can use `Hookable<T>` or `createHooks<T>()` to provide types interface **([c2e1e22](https://github.com/unjs/hookable/commit/c2e1e223d16e7bf87117cd8d72ad3ba211a333d8))**
241
+ - We no longer provide an IE11 compatible umd build. Instead, you should use an ESM-aware bundler such as webpack or rollup to transpile if needed.
242
+ - Logger param is dropped. We use `console.warn` by default for deprecated hooks.
243
+ - Package now uses named exports. You should import `{ Hookable }` instead of `Hookable` or use new `createHooks` util
244
+ - `mergeHooks` util is exported standalone. You should replace `Hookable.mergeHooks` and `this.mergeHooks` with new `{ mergeHooks }` export
245
+ - In versions < 5.0.0 when using `callHook` if an error happened by one of the hook callbacks, we was handling errors globally and call global `error` hook + `console.error` instead and resolve `callHook` promise! This sometimes makes confusing behavior when we think code worked but it didn't. v5 introduced a breaking change that when a hook throws an error, `callHook` also rejects instead of a global `error` event. This means you should be careful to handle all errors when using `callHook` now.
246
+
247
+ ## Credits
248
+
249
+ Extracted from [Nuxt](https://github.com/nuxt/nuxt.js) hooks system originally introduced by [Sébastien Chopin](https://github.com/Atinux)
250
+
251
+ Thanks to [Joe Paice](https://github.com/RGBboy) for donating [hookable](https://www.npmjs.com/package/hookable) package name.
252
+
253
+ ## License
254
+
255
+ MIT - Made with 💖
256
+
257
+ <!-- Badges -->
258
+ [npm-version-src]: https://flat.badgen.net/npm/dt/hookable
259
+ [npm-version-href]: https://npmjs.com/package/hookable
260
+
261
+ [npm-downloads-src]: https://flat.badgen.net/npm/v/hookable
262
+ [npm-downloads-href]: https://npmjs.com/package/hookable
263
+
264
+ [github-actions-ci-src]: https://flat.badgen.net/github/checks/unjs/hookable/main
265
+ [github-actions-ci-href]: https://github.com/unjs/hookable/actions
266
+
267
+ [codecov-src]: https://flat.badgen.net/codecov/c/github/unjs/hookable
268
+ [codecov-href]: https://codecov.io/gh/unjs/hookable
269
+
270
+ [packagephobia-src]: https://flat.badgen.net/packagephobia/install/hookable
271
+ [packagephobia-href]: https://packagephobia.now.sh/result?p=hookable
package/dist/index.cjs ADDED
@@ -0,0 +1,251 @@
1
+ 'use strict';
2
+
3
+ function flatHooks(configHooks, hooks = {}, parentName) {
4
+ for (const key in configHooks) {
5
+ const subHook = configHooks[key];
6
+ const name = parentName ? `${parentName}:${key}` : key;
7
+ if (typeof subHook === "object" && subHook !== null) {
8
+ flatHooks(subHook, hooks, name);
9
+ } else if (typeof subHook === "function") {
10
+ hooks[name] = subHook;
11
+ }
12
+ }
13
+ return hooks;
14
+ }
15
+ function mergeHooks(...hooks) {
16
+ const finalHooks = {};
17
+ for (const hook of hooks) {
18
+ const flatenHook = flatHooks(hook);
19
+ for (const key in flatenHook) {
20
+ if (finalHooks[key]) {
21
+ finalHooks[key].push(flatenHook[key]);
22
+ } else {
23
+ finalHooks[key] = [flatenHook[key]];
24
+ }
25
+ }
26
+ }
27
+ for (const key in finalHooks) {
28
+ if (finalHooks[key].length > 1) {
29
+ const array = finalHooks[key];
30
+ finalHooks[key] = (...arguments_) => serial(array, (function_) => function_(...arguments_));
31
+ } else {
32
+ finalHooks[key] = finalHooks[key][0];
33
+ }
34
+ }
35
+ return finalHooks;
36
+ }
37
+ function serial(tasks, function_) {
38
+ return tasks.reduce((promise, task) => promise.then(() => function_(task)), Promise.resolve());
39
+ }
40
+ function serialCaller(hooks, arguments_) {
41
+ return hooks.reduce((promise, hookFunction) => promise.then(() => hookFunction.apply(void 0, arguments_)), Promise.resolve());
42
+ }
43
+ function parallelCaller(hooks, arguments_) {
44
+ return Promise.all(hooks.map((hook) => hook.apply(void 0, arguments_)));
45
+ }
46
+ function callEachWith(callbacks, argument0) {
47
+ for (const callback of callbacks) {
48
+ callback(argument0);
49
+ }
50
+ }
51
+
52
+ class Hookable {
53
+ constructor() {
54
+ this._hooks = {};
55
+ this._before = void 0;
56
+ this._after = void 0;
57
+ this._deprecatedMessages = void 0;
58
+ this._deprecatedHooks = {};
59
+ this.hook = this.hook.bind(this);
60
+ this.callHook = this.callHook.bind(this);
61
+ this.callHookWith = this.callHookWith.bind(this);
62
+ }
63
+ hook(name, function_, options = {}) {
64
+ if (!name || typeof function_ !== "function") {
65
+ return () => {
66
+ };
67
+ }
68
+ const originalName = name;
69
+ let dep;
70
+ while (this._deprecatedHooks[name]) {
71
+ dep = this._deprecatedHooks[name];
72
+ name = dep.to;
73
+ }
74
+ if (dep && !options.allowDeprecated) {
75
+ let message = dep.message;
76
+ if (!message) {
77
+ message = `${originalName} hook has been deprecated` + (dep.to ? `, please use ${dep.to}` : "");
78
+ }
79
+ if (!this._deprecatedMessages) {
80
+ this._deprecatedMessages = /* @__PURE__ */ new Set();
81
+ }
82
+ if (!this._deprecatedMessages.has(message)) {
83
+ console.warn(message);
84
+ this._deprecatedMessages.add(message);
85
+ }
86
+ }
87
+ this._hooks[name] = this._hooks[name] || [];
88
+ this._hooks[name].push(function_);
89
+ return () => {
90
+ if (function_) {
91
+ this.removeHook(name, function_);
92
+ function_ = void 0;
93
+ }
94
+ };
95
+ }
96
+ hookOnce(name, function_) {
97
+ let _unreg;
98
+ let _function = (...arguments_) => {
99
+ if (typeof _unreg === "function") {
100
+ _unreg();
101
+ }
102
+ _unreg = void 0;
103
+ _function = void 0;
104
+ return function_(...arguments_);
105
+ };
106
+ _unreg = this.hook(name, _function);
107
+ return _unreg;
108
+ }
109
+ removeHook(name, function_) {
110
+ if (this._hooks[name]) {
111
+ const index = this._hooks[name].indexOf(function_);
112
+ if (index !== -1) {
113
+ this._hooks[name].splice(index, 1);
114
+ }
115
+ if (this._hooks[name].length === 0) {
116
+ delete this._hooks[name];
117
+ }
118
+ }
119
+ }
120
+ deprecateHook(name, deprecated) {
121
+ this._deprecatedHooks[name] = typeof deprecated === "string" ? { to: deprecated } : deprecated;
122
+ const _hooks = this._hooks[name] || [];
123
+ this._hooks[name] = void 0;
124
+ for (const hook of _hooks) {
125
+ this.hook(name, hook);
126
+ }
127
+ }
128
+ deprecateHooks(deprecatedHooks) {
129
+ Object.assign(this._deprecatedHooks, deprecatedHooks);
130
+ for (const name in deprecatedHooks) {
131
+ this.deprecateHook(name, deprecatedHooks[name]);
132
+ }
133
+ }
134
+ addHooks(configHooks) {
135
+ const hooks = flatHooks(configHooks);
136
+ const removeFns = Object.keys(hooks).map((key) => this.hook(key, hooks[key]));
137
+ return () => {
138
+ for (const unreg of removeFns.splice(0, removeFns.length)) {
139
+ unreg();
140
+ }
141
+ };
142
+ }
143
+ removeHooks(configHooks) {
144
+ const hooks = flatHooks(configHooks);
145
+ for (const key in hooks) {
146
+ this.removeHook(key, hooks[key]);
147
+ }
148
+ }
149
+ callHook(name, ...arguments_) {
150
+ return this.callHookWith(serialCaller, name, ...arguments_);
151
+ }
152
+ callHookParallel(name, ...arguments_) {
153
+ return this.callHookWith(parallelCaller, name, ...arguments_);
154
+ }
155
+ callHookWith(caller, name, ...arguments_) {
156
+ const event = this._before || this._after ? { name, args: arguments_, context: {} } : void 0;
157
+ if (this._before) {
158
+ callEachWith(this._before, event);
159
+ }
160
+ const result = caller(this._hooks[name] || [], arguments_);
161
+ if (result instanceof Promise) {
162
+ return result.finally(() => {
163
+ if (this._after && event) {
164
+ callEachWith(this._after, event);
165
+ }
166
+ });
167
+ }
168
+ if (this._after && event) {
169
+ callEachWith(this._after, event);
170
+ }
171
+ return result;
172
+ }
173
+ beforeEach(function_) {
174
+ this._before = this._before || [];
175
+ this._before.push(function_);
176
+ return () => {
177
+ const index = this._before.indexOf(function_);
178
+ if (index !== -1) {
179
+ this._before.splice(index, 1);
180
+ }
181
+ };
182
+ }
183
+ afterEach(function_) {
184
+ this._after = this._after || [];
185
+ this._after.push(function_);
186
+ return () => {
187
+ const index = this._after.indexOf(function_);
188
+ if (index !== -1) {
189
+ this._after.splice(index, 1);
190
+ }
191
+ };
192
+ }
193
+ }
194
+ function createHooks() {
195
+ return new Hookable();
196
+ }
197
+
198
+ const isBrowser = typeof window !== "undefined";
199
+ function createDebugger(hooks, _options = {}) {
200
+ const options = {
201
+ inspect: isBrowser,
202
+ group: isBrowser,
203
+ filter: () => true,
204
+ ..._options
205
+ };
206
+ const _filter = options.filter;
207
+ const filter = typeof _filter === "string" ? (name) => name.startsWith(_filter) : _filter;
208
+ const _tag = options.tag ? `[${options.tag}] ` : "";
209
+ const logPrefix = (event) => _tag + event.name + "".padEnd(event._id, "\0");
210
+ const _idCtr = {};
211
+ const unsubscribeBefore = hooks.beforeEach((event) => {
212
+ if (!filter(event.name)) {
213
+ return;
214
+ }
215
+ _idCtr[event.name] = _idCtr[event.name] || 0;
216
+ event._id = _idCtr[event.name]++;
217
+ console.time(logPrefix(event));
218
+ });
219
+ const unsubscribeAfter = hooks.afterEach((event) => {
220
+ if (!filter(event.name)) {
221
+ return;
222
+ }
223
+ if (options.group) {
224
+ console.groupCollapsed(event.name);
225
+ }
226
+ if (options.inspect) {
227
+ console.timeLog(logPrefix(event), event.args);
228
+ } else {
229
+ console.timeEnd(logPrefix(event));
230
+ }
231
+ if (options.group) {
232
+ console.groupEnd();
233
+ }
234
+ _idCtr[event.name]--;
235
+ });
236
+ return {
237
+ close: () => {
238
+ unsubscribeBefore();
239
+ unsubscribeAfter();
240
+ }
241
+ };
242
+ }
243
+
244
+ exports.Hookable = Hookable;
245
+ exports.createDebugger = createDebugger;
246
+ exports.createHooks = createHooks;
247
+ exports.flatHooks = flatHooks;
248
+ exports.mergeHooks = mergeHooks;
249
+ exports.parallelCaller = parallelCaller;
250
+ exports.serial = serial;
251
+ exports.serialCaller = serialCaller;
@@ -0,0 +1,100 @@
1
+ declare type HookCallback = (...arguments_: any) => Promise<void> | void;
2
+ interface Hooks {
3
+ [key: string]: HookCallback;
4
+ }
5
+ declare type HookKeys<T> = keyof T & string;
6
+ declare type DeprecatedHook<T> = {
7
+ message?: string;
8
+ to: HookKeys<T>;
9
+ };
10
+ declare type DeprecatedHooks<T> = {
11
+ [name in HookKeys<T>]: DeprecatedHook<T>;
12
+ };
13
+ declare type ValueOf<C> = C extends Record<any, any> ? C[keyof C] : never;
14
+ declare type Strings<T> = Exclude<keyof T, number | symbol>;
15
+ declare type KnownKeys<T> = keyof {
16
+ [K in keyof T as string extends K ? never : number extends K ? never : K]: never;
17
+ };
18
+ declare type StripGeneric<T> = Pick<T, KnownKeys<T> extends keyof T ? KnownKeys<T> : never>;
19
+ declare type OnlyGeneric<T> = Omit<T, KnownKeys<T> extends keyof T ? KnownKeys<T> : never>;
20
+ declare type Namespaces<T> = ValueOf<{
21
+ [key in Strings<T>]: key extends `${infer Namespace}:${string}` ? Namespace : never;
22
+ }>;
23
+ declare type BareHooks<T> = ValueOf<{
24
+ [key in Strings<T>]: key extends `${string}:${string}` ? never : key;
25
+ }>;
26
+ declare type HooksInNamespace<T, Namespace extends string> = ValueOf<{
27
+ [key in Strings<T>]: key extends `${Namespace}:${infer HookName}` ? HookName : never;
28
+ }>;
29
+ declare type WithoutNamespace<T, Namespace extends string> = {
30
+ [key in HooksInNamespace<T, Namespace>]: `${Namespace}:${key}` extends keyof T ? T[`${Namespace}:${key}`] : never;
31
+ };
32
+ declare type NestedHooks<T> = (Partial<StripGeneric<T>> | Partial<OnlyGeneric<T>>) & Partial<{
33
+ [key in Namespaces<StripGeneric<T>>]: NestedHooks<WithoutNamespace<T, key>>;
34
+ }> & Partial<{
35
+ [key in BareHooks<StripGeneric<T>>]: T[key];
36
+ }>;
37
+
38
+ declare type InferCallback<HT, HN extends keyof HT> = HT[HN] extends HookCallback ? HT[HN] : never;
39
+ declare type InferSpyEvent<HT extends Record<string, any>> = {
40
+ [key in keyof HT]: {
41
+ name: key;
42
+ args: Parameters<HT[key]>;
43
+ context: Record<string, any>;
44
+ };
45
+ }[keyof HT];
46
+ declare class Hookable<HooksT = Record<string, HookCallback>, HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>> {
47
+ private _hooks;
48
+ private _before;
49
+ private _after;
50
+ private _deprecatedHooks;
51
+ private _deprecatedMessages;
52
+ constructor();
53
+ hook<NameT extends HookNameT>(name: NameT, function_: InferCallback<HooksT, NameT>, options?: {
54
+ allowDeprecated?: boolean;
55
+ }): () => void;
56
+ hookOnce<NameT extends HookNameT>(name: NameT, function_: InferCallback<HooksT, NameT>): () => void;
57
+ removeHook<NameT extends HookNameT>(name: NameT, function_: InferCallback<HooksT, NameT>): void;
58
+ deprecateHook<NameT extends HookNameT>(name: NameT, deprecated: HookKeys<HooksT> | DeprecatedHook<HooksT>): void;
59
+ deprecateHooks(deprecatedHooks: Partial<Record<HookNameT, DeprecatedHook<HooksT>>>): void;
60
+ addHooks(configHooks: NestedHooks<HooksT>): () => void;
61
+ removeHooks(configHooks: NestedHooks<HooksT>): void;
62
+ callHook<NameT extends HookNameT>(name: NameT, ...arguments_: Parameters<InferCallback<HooksT, NameT>>): Promise<any>;
63
+ callHookParallel<NameT extends HookNameT>(name: NameT, ...arguments_: Parameters<InferCallback<HooksT, NameT>>): Promise<any[]>;
64
+ callHookWith<NameT extends HookNameT, CallFunction extends (hooks: HookCallback[], arguments_: Parameters<InferCallback<HooksT, NameT>>) => any>(caller: CallFunction, name: NameT, ...arguments_: Parameters<InferCallback<HooksT, NameT>>): ReturnType<CallFunction>;
65
+ beforeEach(function_: (event: InferSpyEvent<HooksT>) => void): () => void;
66
+ afterEach(function_: (event: InferSpyEvent<HooksT>) => void): () => void;
67
+ }
68
+ declare function createHooks<T>(): Hookable<T>;
69
+
70
+ declare function flatHooks<T>(configHooks: NestedHooks<T>, hooks?: T, parentName?: string): T;
71
+ declare function mergeHooks<T>(...hooks: NestedHooks<T>[]): T;
72
+ declare function serial<T>(tasks: T[], function_: (task: T) => Promise<any> | any): Promise<any>;
73
+ declare function serialCaller(hooks: HookCallback[], arguments_?: any[]): Promise<any>;
74
+ declare function parallelCaller(hooks: HookCallback[], arguments_?: any[]): Promise<any[]>;
75
+
76
+ interface CreateDebuggerOptions {
77
+ /** An optional tag to prefix console logs with */
78
+ tag?: string;
79
+ /**
80
+ * Show hook params to the console output
81
+ *
82
+ * Enabled for browsers by default
83
+ */
84
+ inspect?: boolean;
85
+ /**
86
+ * Use group/groupEnd wrapper around logs happening during a specific hook
87
+ *
88
+ * Enabled for browsers by default
89
+ */
90
+ group?: boolean;
91
+ /** Filter which hooks to enable debugger for. Can be a string prefix or fn. */
92
+ filter?: string | ((event: string) => boolean);
93
+ }
94
+ /** Start debugging hook names and timing in console */
95
+ declare function createDebugger(hooks: Hookable<any>, _options?: CreateDebuggerOptions): {
96
+ /** Stop debugging and remove listeners */
97
+ close: () => void;
98
+ };
99
+
100
+ export { CreateDebuggerOptions, DeprecatedHook, DeprecatedHooks, HookCallback, HookKeys, Hookable, Hooks, NestedHooks, createDebugger, createHooks, flatHooks, mergeHooks, parallelCaller, serial, serialCaller };
package/dist/index.mjs ADDED
@@ -0,0 +1,242 @@
1
+ function flatHooks(configHooks, hooks = {}, parentName) {
2
+ for (const key in configHooks) {
3
+ const subHook = configHooks[key];
4
+ const name = parentName ? `${parentName}:${key}` : key;
5
+ if (typeof subHook === "object" && subHook !== null) {
6
+ flatHooks(subHook, hooks, name);
7
+ } else if (typeof subHook === "function") {
8
+ hooks[name] = subHook;
9
+ }
10
+ }
11
+ return hooks;
12
+ }
13
+ function mergeHooks(...hooks) {
14
+ const finalHooks = {};
15
+ for (const hook of hooks) {
16
+ const flatenHook = flatHooks(hook);
17
+ for (const key in flatenHook) {
18
+ if (finalHooks[key]) {
19
+ finalHooks[key].push(flatenHook[key]);
20
+ } else {
21
+ finalHooks[key] = [flatenHook[key]];
22
+ }
23
+ }
24
+ }
25
+ for (const key in finalHooks) {
26
+ if (finalHooks[key].length > 1) {
27
+ const array = finalHooks[key];
28
+ finalHooks[key] = (...arguments_) => serial(array, (function_) => function_(...arguments_));
29
+ } else {
30
+ finalHooks[key] = finalHooks[key][0];
31
+ }
32
+ }
33
+ return finalHooks;
34
+ }
35
+ function serial(tasks, function_) {
36
+ return tasks.reduce((promise, task) => promise.then(() => function_(task)), Promise.resolve());
37
+ }
38
+ function serialCaller(hooks, arguments_) {
39
+ return hooks.reduce((promise, hookFunction) => promise.then(() => hookFunction.apply(void 0, arguments_)), Promise.resolve());
40
+ }
41
+ function parallelCaller(hooks, arguments_) {
42
+ return Promise.all(hooks.map((hook) => hook.apply(void 0, arguments_)));
43
+ }
44
+ function callEachWith(callbacks, argument0) {
45
+ for (const callback of callbacks) {
46
+ callback(argument0);
47
+ }
48
+ }
49
+
50
+ class Hookable {
51
+ constructor() {
52
+ this._hooks = {};
53
+ this._before = void 0;
54
+ this._after = void 0;
55
+ this._deprecatedMessages = void 0;
56
+ this._deprecatedHooks = {};
57
+ this.hook = this.hook.bind(this);
58
+ this.callHook = this.callHook.bind(this);
59
+ this.callHookWith = this.callHookWith.bind(this);
60
+ }
61
+ hook(name, function_, options = {}) {
62
+ if (!name || typeof function_ !== "function") {
63
+ return () => {
64
+ };
65
+ }
66
+ const originalName = name;
67
+ let dep;
68
+ while (this._deprecatedHooks[name]) {
69
+ dep = this._deprecatedHooks[name];
70
+ name = dep.to;
71
+ }
72
+ if (dep && !options.allowDeprecated) {
73
+ let message = dep.message;
74
+ if (!message) {
75
+ message = `${originalName} hook has been deprecated` + (dep.to ? `, please use ${dep.to}` : "");
76
+ }
77
+ if (!this._deprecatedMessages) {
78
+ this._deprecatedMessages = /* @__PURE__ */ new Set();
79
+ }
80
+ if (!this._deprecatedMessages.has(message)) {
81
+ console.warn(message);
82
+ this._deprecatedMessages.add(message);
83
+ }
84
+ }
85
+ this._hooks[name] = this._hooks[name] || [];
86
+ this._hooks[name].push(function_);
87
+ return () => {
88
+ if (function_) {
89
+ this.removeHook(name, function_);
90
+ function_ = void 0;
91
+ }
92
+ };
93
+ }
94
+ hookOnce(name, function_) {
95
+ let _unreg;
96
+ let _function = (...arguments_) => {
97
+ if (typeof _unreg === "function") {
98
+ _unreg();
99
+ }
100
+ _unreg = void 0;
101
+ _function = void 0;
102
+ return function_(...arguments_);
103
+ };
104
+ _unreg = this.hook(name, _function);
105
+ return _unreg;
106
+ }
107
+ removeHook(name, function_) {
108
+ if (this._hooks[name]) {
109
+ const index = this._hooks[name].indexOf(function_);
110
+ if (index !== -1) {
111
+ this._hooks[name].splice(index, 1);
112
+ }
113
+ if (this._hooks[name].length === 0) {
114
+ delete this._hooks[name];
115
+ }
116
+ }
117
+ }
118
+ deprecateHook(name, deprecated) {
119
+ this._deprecatedHooks[name] = typeof deprecated === "string" ? { to: deprecated } : deprecated;
120
+ const _hooks = this._hooks[name] || [];
121
+ this._hooks[name] = void 0;
122
+ for (const hook of _hooks) {
123
+ this.hook(name, hook);
124
+ }
125
+ }
126
+ deprecateHooks(deprecatedHooks) {
127
+ Object.assign(this._deprecatedHooks, deprecatedHooks);
128
+ for (const name in deprecatedHooks) {
129
+ this.deprecateHook(name, deprecatedHooks[name]);
130
+ }
131
+ }
132
+ addHooks(configHooks) {
133
+ const hooks = flatHooks(configHooks);
134
+ const removeFns = Object.keys(hooks).map((key) => this.hook(key, hooks[key]));
135
+ return () => {
136
+ for (const unreg of removeFns.splice(0, removeFns.length)) {
137
+ unreg();
138
+ }
139
+ };
140
+ }
141
+ removeHooks(configHooks) {
142
+ const hooks = flatHooks(configHooks);
143
+ for (const key in hooks) {
144
+ this.removeHook(key, hooks[key]);
145
+ }
146
+ }
147
+ callHook(name, ...arguments_) {
148
+ return this.callHookWith(serialCaller, name, ...arguments_);
149
+ }
150
+ callHookParallel(name, ...arguments_) {
151
+ return this.callHookWith(parallelCaller, name, ...arguments_);
152
+ }
153
+ callHookWith(caller, name, ...arguments_) {
154
+ const event = this._before || this._after ? { name, args: arguments_, context: {} } : void 0;
155
+ if (this._before) {
156
+ callEachWith(this._before, event);
157
+ }
158
+ const result = caller(this._hooks[name] || [], arguments_);
159
+ if (result instanceof Promise) {
160
+ return result.finally(() => {
161
+ if (this._after && event) {
162
+ callEachWith(this._after, event);
163
+ }
164
+ });
165
+ }
166
+ if (this._after && event) {
167
+ callEachWith(this._after, event);
168
+ }
169
+ return result;
170
+ }
171
+ beforeEach(function_) {
172
+ this._before = this._before || [];
173
+ this._before.push(function_);
174
+ return () => {
175
+ const index = this._before.indexOf(function_);
176
+ if (index !== -1) {
177
+ this._before.splice(index, 1);
178
+ }
179
+ };
180
+ }
181
+ afterEach(function_) {
182
+ this._after = this._after || [];
183
+ this._after.push(function_);
184
+ return () => {
185
+ const index = this._after.indexOf(function_);
186
+ if (index !== -1) {
187
+ this._after.splice(index, 1);
188
+ }
189
+ };
190
+ }
191
+ }
192
+ function createHooks() {
193
+ return new Hookable();
194
+ }
195
+
196
+ const isBrowser = typeof window !== "undefined";
197
+ function createDebugger(hooks, _options = {}) {
198
+ const options = {
199
+ inspect: isBrowser,
200
+ group: isBrowser,
201
+ filter: () => true,
202
+ ..._options
203
+ };
204
+ const _filter = options.filter;
205
+ const filter = typeof _filter === "string" ? (name) => name.startsWith(_filter) : _filter;
206
+ const _tag = options.tag ? `[${options.tag}] ` : "";
207
+ const logPrefix = (event) => _tag + event.name + "".padEnd(event._id, "\0");
208
+ const _idCtr = {};
209
+ const unsubscribeBefore = hooks.beforeEach((event) => {
210
+ if (!filter(event.name)) {
211
+ return;
212
+ }
213
+ _idCtr[event.name] = _idCtr[event.name] || 0;
214
+ event._id = _idCtr[event.name]++;
215
+ console.time(logPrefix(event));
216
+ });
217
+ const unsubscribeAfter = hooks.afterEach((event) => {
218
+ if (!filter(event.name)) {
219
+ return;
220
+ }
221
+ if (options.group) {
222
+ console.groupCollapsed(event.name);
223
+ }
224
+ if (options.inspect) {
225
+ console.timeLog(logPrefix(event), event.args);
226
+ } else {
227
+ console.timeEnd(logPrefix(event));
228
+ }
229
+ if (options.group) {
230
+ console.groupEnd();
231
+ }
232
+ _idCtr[event.name]--;
233
+ });
234
+ return {
235
+ close: () => {
236
+ unsubscribeBefore();
237
+ unsubscribeAfter();
238
+ }
239
+ };
240
+ }
241
+
242
+ export { Hookable, createDebugger, createHooks, flatHooks, mergeHooks, parallelCaller, serial, serialCaller };
package/package.json CHANGED
@@ -1,22 +1,45 @@
1
1
  {
2
2
  "name": "hookable",
3
- "version": "0.0.1",
4
- "description": "Enable hookable functions on an object.",
5
- "keywords": ["hook", "plugin"],
6
- "author": "RGBboy <me@rgbboy.com>",
7
- "main": "index.js",
8
- "engines": {
9
- "node": "0.8.x"
3
+ "version": "1.0.0",
4
+ "description": "Awaitable hook system",
5
+ "keywords": [
6
+ "hook",
7
+ "hookable",
8
+ "plugin",
9
+ "tapable",
10
+ "tappable"
11
+ ],
12
+ "repository": "unjs/hookable",
13
+ "license": "MIT",
14
+ "exports": {
15
+ "import": "./dist/index.mjs",
16
+ "types": "./dist/index.d.ts",
17
+ "require": "./dist/index.cjs"
10
18
  },
11
- "dependencies": {
19
+ "main": "./dist/index.cjs",
20
+ "module": "./dist/index.mjs",
21
+ "types": "./dist/index.d.ts",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "unbuild",
27
+ "dev": "vitest",
28
+ "lint": "eslint --ext .ts src",
29
+ "prepublish": "pnpm build",
30
+ "release": "pnpm test && pnpm build && standard-version && git push --follow-tags && pnpm publish",
31
+ "test": "pnpm lint && vitest run --coverage",
32
+ "test:types": "tsc --noEmit"
12
33
  },
13
34
  "devDependencies": {
14
- "mocha": "1.5.x",
15
- "should": "1.2.x"
35
+ "@vitest/coverage-c8": "^0.25.2",
36
+ "eslint": "^8.27.0",
37
+ "eslint-config-unjs": "^0.0.2",
38
+ "expect-type": "^0.15.0",
39
+ "standard-version": "^9.5.0",
40
+ "typescript": "^4.8.4",
41
+ "unbuild": "^0.9.4",
42
+ "vitest": "^0.25.2"
16
43
  },
17
- "scripts" : {
18
- "test" : "make test",
19
- "unit" : "make unit",
20
- "spec" : "make spec"
21
- }
22
- }
44
+ "packageManager": "pnpm@7.16.0"
45
+ }
package/.npmignore DELETED
@@ -1,7 +0,0 @@
1
- .git*
2
- .travis*
3
- Makefile
4
- test/
5
- example/
6
- .DS_Store
7
- *.tmproj
package/index.js DELETED
@@ -1,2 +0,0 @@
1
-
2
- exports = module.exports = require('./lib/hookable');
package/lib/hookable.js DELETED
@@ -1,29 +0,0 @@
1
- /*!
2
- * hookable
3
- * Copyright(c) 2012 RGBboy <me@rgbboy.com>
4
- * MIT Licensed
5
- */
6
-
7
- /**
8
- * Module Dependencies
9
- */
10
-
11
-
12
-
13
- /**
14
- * Add hookable functionality to component instance.
15
- *
16
- * @param {Object} that
17
- * @api public
18
- */
19
- exports = module.exports = function (that) {
20
-
21
- return that;
22
-
23
- };
24
-
25
- /**
26
- * Library version.
27
- */
28
-
29
- exports.version = '0.0.1';
package/readme.md DELETED
@@ -1,19 +0,0 @@
1
- # Hookable
2
-
3
- Enable hookable functions on an object.
4
-
5
- [![Build Status](https://secure.travis-ci.org/RGBboy/hookable.png)](http://travis-ci.org/RGBboy/hookable)
6
-
7
- ## Installation
8
-
9
- Works with Express 3.0.x
10
-
11
- $ npm install git://github.com/RGBboy/hookable.git
12
-
13
- ## Usage
14
-
15
- **Write this**
16
-
17
- ## To Do
18
-
19
- * Write tests