Skip to content

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:

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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, either result or error;
  • extraOptions: Additional options passed by ClassExecutor.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.
ts
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>
}

Contributors

Changelog