A WordPress-style hooks system for Node.js applications. Build modular, traceable, and extensible apps with lifecycle hooks, services, and features.
npm install hook-appimport hookApp from 'hook-app';Create a simple app with a feature that runs during initialization:
import hookApp, { type RegisterContext } from 'hook-app';
const myFeature = ({ registerAction }: RegisterContext) => {
registerAction({
hook: '$INIT_FEATURE',
handler: () => {
console.log('Hello, Hook App!');
},
});
};
await hookApp({ features: [myFeature] });Named extension points where actions can attach. Reference built-in hooks with $HOOK_NAME syntax:
registerAction({
hook: '$INIT_FEATURE', // Built-in lifecycle hook
handler: () => { /* ... */ },
});Functions that execute when a hook is triggered. Actions have a name, priority, and handler:
registerAction({
hook: '$START',
name: 'my-action',
priority: 10, // Higher = runs first
handler: (args, ctx) => {
// Your logic here
},
});- Services - Core integrations that set up shared functionality (databases, logging, etc.)
- Features - Application-specific logic built on top of services
Both use the same RegisterContext interface:
const myService = ({ registerAction, registerHook, createHook }: RegisterContext) => {
// Register custom hooks, actions, etc.
};
await hookApp({
services: [myService],
features: [myFeature],
});Boot phases execute in this order:
START → SETTINGS → INIT_SERVICES → INIT_SERVICE → INIT_FEATURES → INIT_FEATURE
→ START_SERVICES → START_SERVICE → START_FEATURES → START_FEATURE → FINISH
Features communicate through custom hooks:
import hookApp, { type RegisterContext } from 'hook-app';
// Feature 1: Registers and triggers a custom hook
const notifier = ({ registerHook, registerAction, createHook }: RegisterContext) => {
registerHook({ NOTIFY: 'notify' });
registerAction({
hook: '$INIT_FEATURE',
handler: () => {
createHook.sync('notify', { message: 'Hello from notifier!' });
},
});
};
// Feature 2: Listens to the custom hook
const listener = ({ registerAction }: RegisterContext) => {
registerAction({
hook: '$NOTIFY',
handler: (args) => {
console.log('Received:', (args as { message: string }).message);
},
});
};
await hookApp({ features: [notifier, listener] });Access and modify settings using dot-notation paths:
import hookApp, { type RegisterContext } from 'hook-app';
const app = await hookApp({
settings: {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000,
},
},
features: [
({ registerAction }: RegisterContext) => {
registerAction({
hook: '$INIT_FEATURE',
handler: ({ getConfig, setConfig }: RegisterContext) => {
const url = getConfig<string>('api.baseUrl');
console.log('API URL:', url);
// Modify settings
setConfig('api.timeout', 10000);
},
});
},
],
});
// Access final settings
console.log(app.settings);Choose how actions execute when a hook is triggered:
| Mode | Method | Description |
|---|---|---|
sync |
createHook.sync(name, args?) |
Synchronous execution |
serie |
createHook.serie(name, args?) |
Async, one at a time |
parallel |
createHook.parallel(name, args?) |
Async, all at once |
waterfall |
createHook.waterfall(name, initial) |
Pass result to next handler |
Waterfall example:
registerAction('transform', (value: number) => value + 1);
registerAction('transform', (value: number) => value * 2);
const result = createHook.waterfall('transform', 5);
// result.value === 12 (5 + 1 = 6, then 6 * 2 = 12)| Hook | Execution | Description |
|---|---|---|
$START |
serie | App starting |
$SETTINGS |
serie | Configure settings |
$INIT_SERVICES |
parallel | Initialize all services |
$INIT_SERVICE |
serie | Initialize each service |
$INIT_FEATURES |
parallel | Initialize all features |
$INIT_FEATURE |
serie | Initialize each feature |
$START_SERVICES |
parallel | Start all services |
$START_SERVICE |
serie | Start each service |
$START_FEATURES |
parallel | Start all features |
$START_FEATURE |
serie | Start each feature |
$FINISH |
serie | App ready |
import hookApp from 'hook-app';
const app = await hookApp({
services?: ServiceDef[],
features?: FeatureDef[],
settings?: Record<string, unknown> | ((ctx: RegisterContext) => Record<string, unknown>),
context?: Record<string, unknown>,
trace?: string | null,
});
// Returns: { settings, context }// Register an action on a hook
registerAction({
hook: '$INIT_FEATURE',
handler: (args, ctx) => { /* ... */ },
name?: string,
priority?: number,
});
// Shorthand forms
registerAction('hook-name', handler);
registerAction('hook-name', handler, { priority: 10 });
// Register a custom hook
registerHook({ MY_HOOK: 'my-hook' });// Synchronous
const results = createHook.sync('hook-name', args?);
// Async sequential
const results = await createHook.serie('hook-name', args?);
// Async parallel
const results = await createHook.parallel('hook-name', args?);
// Waterfall (pass value through handlers)
const { value, results } = createHook.waterfall('hook-name', initialValue);// Get/set configuration
const value = getConfig<T>('path.to.value', defaultValue?);
setConfig('path.to.value', newValue);
// Get/set custom context
const value = getContext<T>('path.to.value', defaultValue?);
setContext('path.to.value', newValue);Enable boot tracing to debug your app's lifecycle:
await hookApp({
trace: 'compact', // 'full' | 'normal' | 'compact' | null
features: [/* ... */],
});ISC