npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@hexr/hookit

v0.2.0

Published

Application hooks made simple

Downloads

7

Readme

Build Status Coverage Status

Don't like the documentation? Help us make it better by emailing us ([email protected]), creating an issue or creating a pull request

Hookit

Application hooks made simple

:warning: Hookit is suitable for production, but it's still undergoing usage testing. Due to that, consider any 0.x minor release to be a breaking change and review the changelog before upgrading. Patch releases are backwards compatible with their minor counterpart.

What? Why?

The ability to extend the functionality of applications is something tech-lovers, developers, and everyone else have wanted. However, providing an easy mechanism for the extension can be cumbersome.

Enter Hookit. Hookit's goal is to make application lifecycle hooks simple and manageable.

Getting Started

Getting started is simple!

npm install @hexr/hookit --save --production

Hookit is designed to be minimal - there's only one dependency, bluebird, for Promise-based iteration functions. Hopefully installation is lightning:zap: fast!

Documentation

Hookit exports a HookManager - this performs similar functions to the Router in web frameworks (like express) - it takes in information, determines where to send it, and responds with the processed information.

To use it, first instantiate it -

const HookManager = require('@hexr/hookit');
const hooks = new HookManager();

The HookManager constructor doesn't take any options.

Now that you've instantiated your HookManager, it's time to tell it what hooks your application supports. You can do this whenever you want, but you must add the hook before you register extension hooks.

// Valid ways to add a hook
hooks.addHook('before-save', true, beforeSaveMergerFn);
hooks.addHook('register-components', false, registerComponentsMergerFn);
hooks.addHook('bootstrap');

// Function structure
addHook(hookName: String, [hookIsSynchronous: Boolean = false], [mergerFunction: Function = noop])

the addHook method takes 3 arguments - the hook name, whether the hook is synchronous, and the merger function

  • The hook name is how you identify the hook - basically a Hook ID. For example, if you have a content generation application, you might have a before-save hook which allows extensions to modify the data via the hook (for example, spellcheck). It's name is before-save, because it describes when the hook is run.

  • hookIsSynchronous tells HookManager how to process the hooks. A general rule of thumb is if your hooks are providing data (i.e. providing additional components to register), the hook will be asynchronous, but if the hook is modifying data (i.e. text replacements in a before-save hook), it will be synchronous. Sync functions use Promise.reduce (similar to Array.reduce, this functionality is provided by bluebird) and async functions use Promise.mapSeries (similar to Array.map, this functionality is also provided by bluebird)

  • The merger function handles parsing and reducing the data that was provided by all of the hooks. By default, a noop (passthrough) function is used, but you can create your own function. Here's an example of what you might use:

    const registerComponentsMergerFn = (({results: newComponents})), appComponentsList) => appComponentsList.concat(newComponents);

    The first paramater in the merger function will be an Object with the schema

     {
         "errors": [],
         "results": []
     }

    We will go a bit more in depth about the merger function in the executeHook section

Now that HookManager knows what hooks your application uses, your extensions can now hook into them!

const extensions = {
    "extensionA": [Class ExtensionA],
    "extensionB": [Class ExtensionB]
    // ...
};

// First register your own hooks
registerInternalHooks(hooks.generateHookRegisterer());

// Now let your extensions register their hooks
Object.getOwnPropertyNames(extensions).forEach((extension) => {
    extensions[extension].registerHooks(hooks.generateHookRegisterer(extension))
});

// Structure
const registerHook = generateHookRegisterer([caller: String = 'default']);
registerHook(hookName: String, action: Function);

This might seem a bit confusing at first - you might be asking "What's the point of generateHookRegisterer? Why can't I just directly register hooks?" Well the answer is simple - when you're hooking extensions into your application, you need to maintain a level of control over them. As an application developer, you expect certain things to work in certain ways, and extensions can interfere with that. The goal of having generateHookRegisterer is to allow you (the developer) to do be able to associate an extension with its actions. If you don't want to worry about this, you can just do something like const registerHook = hooks.generateHookRegisterer(); and then useregisterHook for your hook registrations

<Example>

There are many use cases for why to use generateHookRegisterer, but one of the simplest ones is namespace collision prevention - say you allow your extensions call into a mount-endpoints hook, where they can add additional endpoints which will be mounted. Your application is documented to expose /authenticate/ as the authentication endpoint. What happens when the oauth extension hooks in, and wants to mount to /authenticate/? Best case scenario, it successfully mounts to /authenticate/, but the handler is never called since your handler takes precedence. Worst case scenario is your application crashes. What can you do to prevent either of these? Add collision detection - for example

// arguments are generated / provided in executeHook
// for brevity, there is no handling of errors that might occur
const mountEndpointsMergerFn = ({results: hookResults}, myEndpoints) => {
// hookResults = [Object: {from: hookName (String), result: hookResult (any)}]
    return hookResults.reduce((endpoints, singleHookResult) => {
        validate(singleHookResult.result);
        singleHookResult.result.forEach(endpoint => {
            // This is really basic collision detection, used for the proof of concept
            if (endpoints[endpoint.name]) {
                endpoint.name = `${singleHookResult.from}-${endpoint.name}`;
            }
            endpoints[endpoint.name] = endpoint;
        });
    }, myEndpoints);
};

// mount-endpoints is an async hook
hooks.addHook('mount-endpoints', false, mountEndpointsMergerFn);

</example>

Now that your extensions have registered their hooks, it's time to execute the hooks at the proper time.

// (super awesome application logic)

// time to register components!

hooks.executeHook('register-components', [myComponents], utilities).then(result => {
    console.log(result) // errors: [], results: []
    if (errors.length > 0) {
        // handle hook errors
    }

    result.results.forEach(newComponent => {
        // register component
    });

    // (more super awesome application logic)
});

// structure for async hooks
hook.executeHook(hookID: String, mergerArgs: Array = [], ...hookArgs: any)
// structure for sync (reduced) hooks
hook.executeHook(hookID: String, mergerArgs: Array = [], initialValue: any, ...hookArgs: any)

Runtime Hook execution is done through the executeHook function of an instantiated HookManager. Based on synchronity of the hook, the registered hooks will be mapped (async) or reduced (sync), with the results being passed to the mergerFunction that was defined in the registration process.

Arguments:

  • hookID - the ID of the hook; this is the first argument that was passed to addHook (i.e. beforeSave)
  • mergerArgs - An array of arguments to pass to the merger function. The merger function will be called like merge(results, ...mergerArgs)
  • (Sync only) initialValue - the initial value for the resolver. In the beforeSave example, this will be the content to be saved (i.e post content)
  • ...hookArgs - the arguments to be passed to the hook; for sync hooks, the hook will be called like fn(currentValue, ...hookArgs), and for async functions, fn(...hookArgs)

TL;DR

  • We don't have this at the moment :grimacing: If you have time, feel free to contribute!

Issues and Support

Feel free to create an issue if you have any questions, feature requests or found a bug. As of now, there's no specific template, but if this gets too much traction, something will be put in place. If you want to contact us directly, shoot us an email - [email protected]

Contributing

Feel free to create a Pull Request if you think any changes should be made. You don't have to explain yourself, but be able to if requested.