@logosdx/hooks 1.0.0-beta.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/CHANGELOG.md +15 -0
- package/LICENSE +12 -0
- package/dist/browser/bundle.js +2 -0
- package/dist/browser/bundle.js.map +1 -0
- package/dist/cjs/index.js +796 -0
- package/dist/esm/index.mjs +830 -0
- package/dist/types/index.d.ts +228 -0
- package/package.json +36 -0
- package/readme.md +88 -0
- package/src/index.ts +440 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { AsyncFunc, FunctionProps } from '@logosdx/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown when a hook extension calls `fail()` or when hook execution fails.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* engine.extend('save', 'before', async (ctx) => {
|
|
7
|
+
* if (!ctx.args[0].isValid) {
|
|
8
|
+
* ctx.fail('Validation failed');
|
|
9
|
+
* }
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* const [, err] = await attempt(() => app.save(data));
|
|
13
|
+
* if (isHookError(err)) {
|
|
14
|
+
* console.log(err.hookName); // 'save'
|
|
15
|
+
* console.log(err.extPoint); // 'before'
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
export declare class HookError extends Error {
|
|
19
|
+
/** Name of the hook where the error occurred */
|
|
20
|
+
hookName?: string;
|
|
21
|
+
/** Extension point where the error occurred: 'before', 'after', or 'error' */
|
|
22
|
+
extPoint?: string;
|
|
23
|
+
/** Original error if `fail()` was called with an Error instance */
|
|
24
|
+
originalError?: Error;
|
|
25
|
+
/** Whether the hook was explicitly aborted via `fail()` */
|
|
26
|
+
aborted: boolean;
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Type guard to check if an error is a HookError.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const [result, err] = await attempt(() => app.save(data));
|
|
34
|
+
* if (isHookError(err)) {
|
|
35
|
+
* console.log(`Hook "${err.hookName}" failed at "${err.extPoint}"`);
|
|
36
|
+
* }
|
|
37
|
+
*/
|
|
38
|
+
export declare const isHookError: (error: unknown) => error is HookError;
|
|
39
|
+
interface HookShape<F extends AsyncFunc> {
|
|
40
|
+
args: Parameters<F>;
|
|
41
|
+
results?: Awaited<ReturnType<F>>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Context object passed to hook extension callbacks.
|
|
45
|
+
* Provides access to arguments, results, and control methods.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* engine.extend('fetch', 'before', async (ctx) => {
|
|
49
|
+
* // Read current arguments
|
|
50
|
+
* const [url, options] = ctx.args;
|
|
51
|
+
*
|
|
52
|
+
* // Modify arguments before the original function runs
|
|
53
|
+
* ctx.setArgs([url, { ...options, cache: 'force-cache' }]);
|
|
54
|
+
*
|
|
55
|
+
* // Or skip the original function entirely
|
|
56
|
+
* if (isCached(url)) {
|
|
57
|
+
* ctx.setResult(getCached(url));
|
|
58
|
+
* ctx.returnEarly();
|
|
59
|
+
* }
|
|
60
|
+
* });
|
|
61
|
+
*/
|
|
62
|
+
export interface HookContext<F extends AsyncFunc> extends HookShape<F> {
|
|
63
|
+
/** Current extension point: 'before', 'after', or 'error' */
|
|
64
|
+
point: keyof Hook<F>;
|
|
65
|
+
/** Error from the original function (only set in 'error' extensions) */
|
|
66
|
+
error?: unknown;
|
|
67
|
+
/** Abort hook execution with an error. Throws a HookError. */
|
|
68
|
+
fail: (error?: unknown) => never;
|
|
69
|
+
/** Replace the arguments passed to the original function */
|
|
70
|
+
setArgs: (next: Parameters<F>) => void;
|
|
71
|
+
/** Replace the result returned from the hook chain */
|
|
72
|
+
setResult: (next: Awaited<ReturnType<F>>) => void;
|
|
73
|
+
/** Skip the original function and return early with the current result */
|
|
74
|
+
returnEarly: () => void;
|
|
75
|
+
/** Remove this extension from the hook (useful with `once` behavior) */
|
|
76
|
+
removeHook: () => void;
|
|
77
|
+
}
|
|
78
|
+
export type HookFn<F extends AsyncFunc> = (ctx: HookContext<F>) => Promise<void>;
|
|
79
|
+
declare class Hook<F extends AsyncFunc> {
|
|
80
|
+
before: Set<HookFn<F>>;
|
|
81
|
+
after: Set<HookFn<F>>;
|
|
82
|
+
error: Set<HookFn<F>>;
|
|
83
|
+
}
|
|
84
|
+
type HookExtOptions<F extends AsyncFunc> = {
|
|
85
|
+
callback: HookFn<F>;
|
|
86
|
+
once?: true;
|
|
87
|
+
ignoreOnFail?: true;
|
|
88
|
+
};
|
|
89
|
+
type HookExtOrOptions<F extends AsyncFunc> = HookFn<F> | HookExtOptions<F>;
|
|
90
|
+
type MakeHookOptions = {
|
|
91
|
+
bindTo?: any;
|
|
92
|
+
};
|
|
93
|
+
type FuncOrNever<T> = T extends AsyncFunc ? T : never;
|
|
94
|
+
/**
|
|
95
|
+
* A lightweight, type-safe hook system for extending function behavior.
|
|
96
|
+
*
|
|
97
|
+
* HookEngine allows you to wrap functions and add extensions that run
|
|
98
|
+
* before, after, or on error. Extensions can modify arguments, change
|
|
99
|
+
* results, or abort execution entirely.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* interface MyApp {
|
|
103
|
+
* save(data: Data): Promise<Result>;
|
|
104
|
+
* load(id: string): Promise<Data>;
|
|
105
|
+
* }
|
|
106
|
+
*
|
|
107
|
+
* const app = new MyAppImpl();
|
|
108
|
+
* const hooks = new HookEngine<MyApp>();
|
|
109
|
+
*
|
|
110
|
+
* // Wrap a method to make it hookable
|
|
111
|
+
* hooks.wrap(app, 'save');
|
|
112
|
+
*
|
|
113
|
+
* // Add a validation extension
|
|
114
|
+
* hooks.extend('save', 'before', async (ctx) => {
|
|
115
|
+
* if (!ctx.args[0].isValid) {
|
|
116
|
+
* ctx.fail('Validation failed');
|
|
117
|
+
* }
|
|
118
|
+
* });
|
|
119
|
+
*
|
|
120
|
+
* // Add logging extension
|
|
121
|
+
* hooks.extend('save', 'after', async (ctx) => {
|
|
122
|
+
* console.log('Saved:', ctx.results);
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* @typeParam Shape - Interface defining the hookable functions
|
|
126
|
+
*/
|
|
127
|
+
export declare class HookEngine<Shape> {
|
|
128
|
+
#private;
|
|
129
|
+
/**
|
|
130
|
+
* Add an extension to a registered hook.
|
|
131
|
+
*
|
|
132
|
+
* Extensions run at specific points in the hook lifecycle:
|
|
133
|
+
* - `before`: Runs before the original function. Can modify args or return early.
|
|
134
|
+
* - `after`: Runs after successful execution. Can modify the result.
|
|
135
|
+
* - `error`: Runs when the original function throws. Can handle or transform errors.
|
|
136
|
+
*
|
|
137
|
+
* @param name - Name of the registered hook to extend
|
|
138
|
+
* @param extensionPoint - When to run: 'before', 'after', or 'error'
|
|
139
|
+
* @param cbOrOpts - Extension callback or options object
|
|
140
|
+
* @returns Cleanup function to remove the extension
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* // Simple callback
|
|
144
|
+
* const cleanup = hooks.extend('save', 'before', async (ctx) => {
|
|
145
|
+
* console.log('About to save:', ctx.args);
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* // With options
|
|
149
|
+
* hooks.extend('save', 'after', {
|
|
150
|
+
* callback: async (ctx) => { console.log('Saved!'); },
|
|
151
|
+
* once: true, // Remove after first run
|
|
152
|
+
* ignoreOnFail: true // Don't throw if this extension fails
|
|
153
|
+
* });
|
|
154
|
+
*
|
|
155
|
+
* // Later: remove the extension
|
|
156
|
+
* cleanup();
|
|
157
|
+
*/
|
|
158
|
+
extend<K extends FunctionProps<Shape>>(name: K, extensionPoint: keyof Hook<FuncOrNever<Shape[K]>>, cbOrOpts: HookExtOrOptions<FuncOrNever<Shape[K]>>): () => void;
|
|
159
|
+
/**
|
|
160
|
+
* Register a function as a hookable and return the wrapped version.
|
|
161
|
+
*
|
|
162
|
+
* The wrapped function behaves identically to the original but allows
|
|
163
|
+
* extensions to be added via `extend()`. Use `wrap()` for a simpler API
|
|
164
|
+
* when working with object methods.
|
|
165
|
+
*
|
|
166
|
+
* @param name - Unique name for this hook (must match a key in Shape)
|
|
167
|
+
* @param cb - The original function to wrap
|
|
168
|
+
* @param opts - Options for the wrapped function
|
|
169
|
+
* @returns Wrapped function with hook support
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* const hooks = new HookEngine<{ fetch: typeof fetch }>();
|
|
173
|
+
*
|
|
174
|
+
* const hookedFetch = hooks.make('fetch', fetch);
|
|
175
|
+
*
|
|
176
|
+
* hooks.extend('fetch', 'before', async (ctx) => {
|
|
177
|
+
* console.log('Fetching:', ctx.args[0]);
|
|
178
|
+
* });
|
|
179
|
+
*
|
|
180
|
+
* await hookedFetch('/api/data');
|
|
181
|
+
*/
|
|
182
|
+
make<K extends FunctionProps<Shape>>(name: K, cb: FuncOrNever<Shape[K]>, opts?: MakeHookOptions): FuncOrNever<Shape[K]>;
|
|
183
|
+
/**
|
|
184
|
+
* Wrap an object method in-place to make it hookable.
|
|
185
|
+
*
|
|
186
|
+
* This is a convenience method that combines `make()` with automatic
|
|
187
|
+
* binding and reassignment. The method is replaced on the instance
|
|
188
|
+
* with the wrapped version.
|
|
189
|
+
*
|
|
190
|
+
* @param instance - Object containing the method to wrap
|
|
191
|
+
* @param name - Name of the method to wrap
|
|
192
|
+
* @param opts - Additional options
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* class UserService {
|
|
196
|
+
* async save(user: User) { ... }
|
|
197
|
+
* }
|
|
198
|
+
*
|
|
199
|
+
* const service = new UserService();
|
|
200
|
+
* const hooks = new HookEngine<UserService>();
|
|
201
|
+
*
|
|
202
|
+
* hooks.wrap(service, 'save');
|
|
203
|
+
*
|
|
204
|
+
* // Now service.save() is hookable
|
|
205
|
+
* hooks.extend('save', 'before', async (ctx) => {
|
|
206
|
+
* console.log('Saving user:', ctx.args[0]);
|
|
207
|
+
* });
|
|
208
|
+
*/
|
|
209
|
+
wrap<K extends FunctionProps<Shape>>(instance: Shape, name: K, opts?: MakeHookOptions): void;
|
|
210
|
+
/**
|
|
211
|
+
* Clear all registered hooks and extensions.
|
|
212
|
+
*
|
|
213
|
+
* After calling this method, all hooks are unregistered and all
|
|
214
|
+
* extensions are removed. Previously wrapped functions will continue
|
|
215
|
+
* to work but without any extensions.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* hooks.wrap(app, 'save');
|
|
219
|
+
* hooks.extend('save', 'before', validator);
|
|
220
|
+
*
|
|
221
|
+
* // Reset for testing
|
|
222
|
+
* hooks.clear();
|
|
223
|
+
*
|
|
224
|
+
* // app.save() still works, but validator no longer runs
|
|
225
|
+
*/
|
|
226
|
+
clear(): void;
|
|
227
|
+
}
|
|
228
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"license": "BSD-3-Clause",
|
|
3
|
+
"homepage": "https://logosdx.dev/",
|
|
4
|
+
"bugs": {
|
|
5
|
+
"url": "https://github.com/logosdx/monorepo/issues",
|
|
6
|
+
"email": "danilo@alonso.network"
|
|
7
|
+
},
|
|
8
|
+
"author": "Danilo Alonso <danilo@alonso.network>",
|
|
9
|
+
"name": "@logosdx/hooks",
|
|
10
|
+
"description": "A lightweight, type-safe hook system for extending function behavior",
|
|
11
|
+
"version": "1.0.0-beta.0",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@logosdx/utils": "^5.0.0"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"hooks",
|
|
17
|
+
"middleware",
|
|
18
|
+
"extensions",
|
|
19
|
+
"before after"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src",
|
|
24
|
+
"readme.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"require": "./dist/cjs/index.js",
|
|
31
|
+
"import": "./dist/esm/index.mjs",
|
|
32
|
+
"types": "./dist/types/index.d.ts",
|
|
33
|
+
"browser": "./dist/browser/bundle.js"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://logosdx.dev">
|
|
3
|
+
<img src="./docs/public/images/logo.png" alt="LogosDX"/>
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<h1 align="center">Logos DX</h1>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
Focused TypeScript utilities for building cross-runtime applications, ETL pipelines, UIs, and more. Use it in browsers, React Native, Node.js, or anywhere JavaScript runs.
|
|
11
|
+
<br/>
|
|
12
|
+
<br/>
|
|
13
|
+
<a href="https://logosdx.dev/">Documentation</a> |
|
|
14
|
+
<a href="https://logosdx.dev/getting-started.html">Getting Started</a> |
|
|
15
|
+
<a href="https://logosdx.dev/cheat-sheet.html">Cheat Sheet</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
**Logos** */lōgōs/ n.*<br/>
|
|
21
|
+
**¹** From the ancient Greek meaning "divine reason" and "rational principle."<br/>
|
|
22
|
+
**²** Represents the fundamental order that governs the universe.<br/>
|
|
23
|
+
**³** The stoics believed it was the rational law underlying all things.<br/>
|
|
24
|
+
|
|
25
|
+
**DX** */di-eks/ n.*<br/>
|
|
26
|
+
**¹** Stands for "developer experience."<br/>
|
|
27
|
+
|
|
28
|
+
**LogosDX** */lōgōs di-eks/ n.*<br/>
|
|
29
|
+
**¹** A rational developer experience.<br/>
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Ready-to-use Packages
|
|
34
|
+
|
|
35
|
+
- `@logosdx/utils`: Production utilities that compose. Resilience built in.
|
|
36
|
+
- `@logosdx/observer`: Events that understand patterns. Queues that manage themselves.
|
|
37
|
+
- `@logosdx/fetch`: HTTP that handles failure. Automatically.
|
|
38
|
+
- `@logosdx/storage`: One API for your many key-value stores.
|
|
39
|
+
- `@logosdx/localize`: Localization utilities for everything from languages to customer-specific strings.
|
|
40
|
+
- `@logosdx/dom`: For those who like to raw-dawg the DOM.
|
|
41
|
+
|
|
42
|
+
## Under-construction
|
|
43
|
+
|
|
44
|
+
- `@logosdx/state-machine`: State management as streams, not stores.
|
|
45
|
+
- `@logosdx/kit`: Bootstrap your app. Type-safe from day one. All the packages in one place.
|
|
46
|
+
|
|
47
|
+
## Roadmap
|
|
48
|
+
|
|
49
|
+
- `@logosdx/react`: All of the above, but for React. Use it in Next.js, React Native, or anywhere else.
|
|
50
|
+
|
|
51
|
+
## LLM Helpers
|
|
52
|
+
|
|
53
|
+
> [!TIP]
|
|
54
|
+
> Don't let AI do your work for you. It's not a replacement for human intelligence. It's a tool to help you.
|
|
55
|
+
|
|
56
|
+
We have LLM helpers available for you to use in Cursor, VSCode, and Claude Code.
|
|
57
|
+
|
|
58
|
+
For more information, see the [LLM Helpers](./llm-helpers/README.md) directory.
|
|
59
|
+
|
|
60
|
+
**Add them to your `.cursor/rules` or `.claude` directory.**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# For Claude
|
|
64
|
+
curl -L "https://codeload.github.com/logosdx/monorepo/tar.gz/refs/heads/master" \
|
|
65
|
+
| tar -xz -C .claude --strip-components=2 "monorepo-master/llm-helpers/*.md"
|
|
66
|
+
|
|
67
|
+
# For Cursor
|
|
68
|
+
curl -L "https://codeload.github.com/logosdx/monorepo/tar.gz/refs/heads/master" \
|
|
69
|
+
| tar -xz -C .cursor/rules --strip-components=2 "monorepo-master/llm-helpers/*.md"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Philosophy
|
|
73
|
+
|
|
74
|
+
- TypeScript-first
|
|
75
|
+
- Resilience built in
|
|
76
|
+
- Tree-shakable
|
|
77
|
+
- Runtime agnostic
|
|
78
|
+
- Small and fast
|
|
79
|
+
- Debuggable, testable, and well-documented
|
|
80
|
+
- Zero external dependencies
|
|
81
|
+
|
|
82
|
+
## Contributing
|
|
83
|
+
|
|
84
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development workflow and release process.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT © [LogosDX](https://logosdx.dev)
|