KnodesCommunity/typedoc-plugins

View on GitHub
packages/pluginutils/src/events-extra.ts

Summary

Maintainability
A
0 mins
Test Coverage
B
85%
import assert from 'assert';

import { Application } from 'typedoc';

type Fn = ( ...args: any[] ) => any;
type MethodKeys<T> = {[k in keyof T]: T[k] extends Fn ? k : never}[keyof T] & string
type Params<T> = T extends Fn ? Parameters<T> : unknown[];
type Ret<T> = T extends Fn ? ReturnType<T> : unknown;
export class EventsExtra {
    private static readonly _apps = new WeakMap<Application, EventsExtra>();
    private _beforeOptionsFreezeArgs: null | any[] = null;

    /**
     * Get events extra for the given application.
     *
     * @param application - The application to bind.
     * @returns the events extra instance.
     */
    public static for( application: Application ){
        const e = this._apps.get( application ) ?? new EventsExtra( application );
        this._apps.set( application, e );
        return e;
    }

    private constructor( private readonly application: Application ){}

    /**
     * Execute a function after the option {@link name} has been set.
     *
     * @param name - The option name to watch.
     * @param cb - The function to execute.
     * @returns this.
     */
    public onSetOption( name: string, cb: ( value: unknown ) => void ){
        // eslint-disable-next-line @typescript-eslint/dot-notation -- Private property
        this._hookInstanceAfter( this.application.options['_setOptions'] as Set<string>, 'add', ( set, v ) => {
            if( v === name ){
                cb( this.application.options.getValue( name ) );
            }
            return set;
        } );
        return this;
    }

    /**
     * Execute a function just after theme have been set.
     *
     * @param cb - The function to execute.
     * @returns this.
     */
    public onThemeReady( cb: () => void ){
        this._hookInstanceAfter( this.application.renderer, 'prepareTheme' as any, ( success: boolean ) => {
            if( success ){
                cb();
            }
            return success;
        } );
        return this;
    }

    /**
     * Execute a function just before options freezing.
     *
     * @param cb - The function to execute.
     * @returns this.
     */
    public beforeOptionsFreeze( cb: () => void ){
        if( this._beforeOptionsFreezeArgs ){
            cb();
        }
        this._hookInstanceBefore( this.application.options, 'freeze', ( ...args ) => {
            this._beforeOptionsFreezeArgs = args;
            cb();
            return args;
        } );
        return this;
    }

    /**
     * Replace the method {@link key} of {@link instance} with a method calling the original method, then the custom {@link hook}.
     * The original method return value is passed as the 1st parameter of the hook.
     *
     * @param instance - The instance to bind.
     * @param key - The method name.
     * @param hook - The function to execute after the original one.
     */
    private _hookInstanceAfter<
        T extends {}, // eslint-disable-line @typescript-eslint/ban-types -- Inspired from jest `spyOn` types.
        K extends MethodKeys<T>,
    >( instance: T, key: K, hook: ( initialRet: Ret<T[K]>, ...args: Params<T[K]> ) => Ret<T[K]> ){
        const bck = ( instance[key] as T[K] & Fn ).bind( instance ) as T[K] & Fn;
        assert( bck );
        ( instance[key] as any ) = ( ...args: Params<T[K]> ) => {
            const ret = bck( ...args );
            return hook( ret, ...args );
        };
    }

    /**
     * Replace the method {@link key} of {@link instance} with a method calling the the custom {@link hook}, then the original method.
     * The hook should return arguments to pass to the original method.
     *
     * @param instance - The instance to bind.
     * @param key - The method name.
     * @param hook - The function to execute before the original one.
     */
    private _hookInstanceBefore<
        T extends {}, // eslint-disable-line @typescript-eslint/ban-types -- Inspired from jest `spyOn` types.
        K extends MethodKeys<T>,
    >( instance: T, key: K, hook: ( ...args: Params<T[K]> ) => Params<T[K]> ){
        const bck = ( instance[key] as T[K] & Fn ).bind( instance ) as T[K] & Fn;
        assert( bck );
        ( instance[key] as any ) = ( ...args: Params<T[K]> ) => {
            const newArgs = hook( ...args );
            return bck( ...newArgs );
        };
    }
}