Plugin Development
WARNING
Note that for plugin development, we assume you already have sufficient intermediate to advanced knowledge of TypeScript, as this involves more advanced content related to IoC core.
The core of unioc consists of three parts:
Container
: There is an internal container that stores all registered classes;Bootstrap
: An abstract class that defines the startup process of an App;Plugin
: Freely modify the bootstrap and container, controlling each stage of a class's initialization, execution, destruction, etc.
Plugins have the same level of authority as the bootstrap, giving them extreme flexibility. Below, we will explain in detail how to write a unioc plugin.
unioc provides a plugin system similar to the Rollup plugin system. However, compared to Rollup's hooks, unioc's hooks use simple yet powerful
single
English words corresponding to each processing stage. Since this is a runtime framework, unlike Rollup which operates at compile time, it doesn't have the complex hook execution logic that Rollup has. Below is a simple diagram describing when each plugin hook will be executed.
Plugin Context
Each plugin hook almost always has a context object that inherits from the basic IContext
(which is shared by both the bootstrap and plugins). Additionally, it also inherits from IInternalLogger
, allowing you to use methods like warn
, error
, etc. to log plugin operations. Below is the type definition for the plugin context:
import type { IBootstrapDeriver, IClassWrapper, IContainer, IContext, IInternalLogger, IPlugin } from 'unioc'
export interface IHandleExecutorOptions {
/**
* The class wrapper.
*/
classWrapper: IClassWrapper
/**
* The `catch` error.
*/
error: unknown
/**
* The current method property key.
*/
propertyKey: PropertyKey
/**
* The current method arguments.
*/
methodArguments: unknown[]
/**
* The extra options.
*/
extraOptions?: Record<string, unknown>
}
export interface IPluginContext extends IContext, IInternalLogger, IBootstrapDeriver {
/**
* ### Get global container
*
* 🌏 Get the global container.
*/
getGlobalContainer(): IContainer
/**
* ### Handle
*
* ❌ A function that is used to handle when a class instance method throw an error.
* It can pass the error to enter the plugin pipe context.
*
* @param options - The position information of the `try/catch block`.
* @returns The plugin handle context.
*/
handle(options: IHandleExecutorOptions): Promise<IPlugin.Handle.Context>
}
The handle
method is used to create an error handling pipeline. When any error that needs to be catch
is encountered during plugin execution, you can call the handle
method to create an error handling pipeline, allowing other plugins the opportunity to handle this error.
NOTE
For example, the error interceptor @Catch
in @unioc/adapter-nestjs
is created using the handle
method.
WARNING
Furthermore, any method executed using IClassExecutor.execute
will be automatically caught and passed to the handle
method.
name
The plugin name must be unique. It's recommended to use :
to separate namespaces, such as unioc:plugin-name
, unioc:plugin-name:sub-plugin
, etc.
enforce
and priority
The execution order of plugins, with optional values of pre
and post
. If left empty, it will execute between the pre
and post
phases by default.
WARNING
The execution order of plugins is determined first by the enforce
value, and then by the priority
value.
install()
The install
hook is executed before the plugin executor is initialized. You can perform all initialization operations here. It has only one parameter: the current bootstrap instance is passed to this method.
import type { IBootstrap, IPluginContext } from 'unioc'
import type { Awaitable } from 'unioc/shared'
export interface IPlugin {
/**
* ### Install
*
* 🔧 When the plugin is installed, the hook will be call.
*/
install?(this: IPluginContext, bootstrap: IBootstrap): Awaitable<unknown>
}
resolve()
When a class attempts to be instantiated, the resolve
hook will be called. Its first parameter is a context object that inherits from a series of types. From a macro perspective, the responsibility of this hook is: to modify the class constructor parameters before the class is instantiated. After modification, these constructor parameters will not be re-resolved.
import type { IAbstractClassWrapper, IConstructorArgumentModifier, IConstructorArgumentsGetter, IPluginContext } from 'unioc'
import type { Awaitable, IClass } from 'unioc/shared'
export interface IPlugin {
/**
* ### Resolve
*
* 🐶 When unioc try to create a class instance, the hook will be call at first.
* @returns You can provide constructor arguments value. The modified arguments will not be `re-resolved` again.
* @param ctx - The context of the plugin. You can get the current class wrapper from the context.
*/
resolve?(this: IPluginContext, ctx: IPlugin.Resolve.Context): Awaitable<unknown>
}
export namespace IPlugin {
export namespace Resolve {
export interface Context<TClass extends IClass = IClass> extends IAbstractClassWrapper<TClass>, IConstructorArgumentModifier, IConstructorArgumentsGetter {}
export type Handler = Exclude<IPlugin['resolve'], undefined>
}
}
construct()
The construct
hook is called before a class is instantiated, after the class constructor has been fully resolved. The purpose of this hook is to modify the class constructor after the class constructor has been fully resolved.
import type { IAbstractClassWrapper, IConstructorArgumentModifier, IConstructorArgumentsGetter, IPluginContext } from 'unioc'
import type { Awaitable, IClass } from 'unioc/shared'
export namespace IPlugin {
export namespace Construct {
export interface Context<TClass extends IClass = IClass> extends IAbstractClassWrapper<TClass>, IConstructorArgumentModifier, IConstructorArgumentsGetter {}
export type Handler = Exclude<IPlugin['construct'], undefined>
}
}
export interface IPlugin {
/**
* ### Construct
*
* 🐶 When after the constructor arguments resolved, the hook will be call.
* @returns You can modify the class constructor when the class constructor is resolved.
* @param ctx - The context of the plugin. You can get the current class wrapper from the context.
*/
construct?(this: IPluginContext, ctx: IPlugin.Construct.Context): Awaitable<unknown>
}
apply()
After a class has been instantiated, before property dependencies are resolved, the apply
hook will be called. The purpose of this hook is to modify class properties after the class has been instantiated.
import type { IAbstractClassWrapper, IConstructorArgumentsGetter, IPluginContext, IPropertyModifier } from 'unioc'
import type { Awaitable, IClass } from 'unioc/shared'
export namespace IPlugin {
export namespace Apply {
export interface Context<TClass extends IClass = IClass> extends IAbstractClassWrapper<TClass>, IPropertyModifier, IConstructorArgumentsGetter {}
export type Handler = Exclude<IPlugin['apply'], undefined>
}
}
export interface IPlugin {
/**
* ### Apply
*
* 🍰 When the instance is created but not resolve property dependencies, the hook will be call.
* @returns You can modify the instance properties value.
* @param ctx - The context of the plugin. You can get or modify current class wrapper from the context.
*/
apply?(this: IPluginContext, ctx: IPlugin.Apply.Context): Awaitable<unknown>
}
transform()
After a class has been instantiated and after property dependencies have been resolved, the transform
hook will be called. The purpose of this hook is to modify class properties after the class has been instantiated.
import type { IAbstractClassWrapper, IConstructorArgumentsGetter, IPluginContext, IPropertyModifier } from 'unioc'
import type { Awaitable, IClass } from 'unioc/shared'
export namespace IPlugin {
export namespace Transform {
export interface Context<TClass extends IClass = IClass> extends IAbstractClassWrapper<TClass>, IPropertyModifier, IConstructorArgumentsGetter {}
export type Handler = Exclude<IPlugin['transform'], undefined>
}
}
export interface IPlugin {
/**
* ### Transform
*
* 🧁 When the instance is created and property dependencies are resolved, the hook will be call.
*
* @returns You can modify the instance properties value.
* @param ctx - The context of the plugin. You can get or modify current class wrapper from the context.
*/
transform?(
this: IPluginContext,
ctx: IPlugin.Transform.Context,
): Awaitable<unknown>
}
ready()
When all the above steps have been completed, it indicates that this class is available for external use. At this point, the ready
hook will be called. It's generally used to perform some initialization operations. For example, if you're using @unioc/adapter-nestjs
, it will execute the onModuleInit
method of NestJS
in this hook.
import type { IClassWrapper, IPluginContext } from 'unioc'
import type { Awaitable, IClass } from 'unioc/shared'
export namespace IPlugin {
export namespace Ready {
export interface Context<TClass extends IClass = IClass> extends IClassWrapper<TClass> {
/**
* ### Get instance
*
* 🔍 Get current class instance.
* @note Instance is absolutely certain to exist in this hook. It is not `null` or `undefined`.
*/
getInstance(): InstanceType<TClass>
}
export type Handler = Exclude<IPlugin['ready'], undefined>
}
}
export interface IPlugin {
/**
* ### Ready
*
* 🧪 When the instance is created and property dependencies are resolved, the hook will be call.
* @param ctx - The wrapper context of the class.
*/
ready?<TClass extends IClass = IClass>(this: IPluginContext, ctx: IPlugin.Ready.Context<TClass>): Awaitable<unknown>
}
invoke()
When executing a method of a class instance using ClassExecutor.execute
, the invoke
hook will be called first. In this hook, you're allowed to modify the arguments of the currently called method. This feature will be used to implement decorators such as @Body
, @Param
, etc. in @unioc/adapter-nestjs
.
import type { IClassWrapper, IPluginContext } from 'unioc'
import type { Awaitable, IClass } from 'unioc/shared'
export namespace IPlugin {
export namespace Invoke {
export interface Context<TClass extends IClass = IClass> extends IClassWrapper<TClass> {
/**
* ### Get method arguments
*
* 🔍 Get current method arguments.
*/
getMethodArguments(): readonly unknown[]
}
export type Handler = Exclude<IPlugin['invoke'], undefined>
}
}
export interface IPlugin {
/**
* ### Invoke
*
* 🔍 When the instance is created and property dependencies are resolved, the hook will be call.
* @param ctx - The wrapper context of the class.
*/
invoke?(
this: IPluginContext,
ctx: IPlugin.Invoke.Context,
): Awaitable<unknown>
}
handle()
All errors thrown when executing a method of a class instance using ClassExecutor.execute
, as well as errors caught by this.handle
, will be passed to the handle
hook. This method has three parameters:
ctx
: The plugin context object;type
: The type of execution result, eitherresult
orerror
;extraOptions
: Additional options passed byClassExecutor.execute
. These are generally used to identify which framework/plugin executed the current error/result, providing space for error handling operations and precise error interception.
import type { IClassWrapper, IPluginContext } from 'unioc'
import type { Awaitable, IClass, IResult } from 'unioc/shared'
type IResultType = IResult['type']
export namespace IPlugin {
export namespace Handle {
export interface Context<TClass extends IClass = IClass> extends IClassWrapper<TClass> {
/**
* ### Get execute result
*
* 🥖 Get the execute result.
*/
getExecuteResult<T = unknown>(): T
/**
* ### Set execute result
*
* 🔧 Set the execute result.
*/
setExecuteResult<T = unknown>(result: T): this
/**
* ### Get property key
*
* 🔍 Get current invoke property key.
*/
getPropertyKey(): PropertyKey
/**
* ### Get method arguments
*
* 🔍 Get current method arguments.
*/
getMethodArguments(): readonly unknown[]
}
export type Handler = Exclude<IPlugin['handle'], undefined>
}
}
export interface IPlugin {
/**
* ### Handle
*
* 🌹 When the instance is created and property dependencies are resolved, the hook will be call.
* @param ctx - The wrapper context of the class.
* @param type - The type of the execution result.
*/
handle?(
this: IPluginContext,
ctx: IPlugin.Handle.Context,
type: IResultType,
extraOptions?: Record<string, unknown>,
): Awaitable<unknown>
}