How to Define and Register Plugins in Cordis: Complete Guide with Examples
Cordis supports three plugin shapes—functions, objects with an apply method, and classes—and registers them via ctx.plugin(), which returns a promise-like Fiber that manages the plugin lifecycle and automatic disposal.
Cordis is a lightweight, extensible plugin system that powers the Cordiverse ecosystem. To define and register plugins in Cordis, you interact with the Context API and the RegistryService to instantiate plugins as fibers with dependency injection and automatic cleanup. This guide covers the three valid plugin definitions, the registration flow, and the lifecycle management implemented in packages/core/src/registry.ts.
Cordis Plugin Shapes and Metadata
Cordis recognizes three distinct plugin definitions, all processed by RegistryService in packages/core/src/registry.ts:
- Function plugins:
(ctx: Context, config: T) => any– Invoked directly with the current context. - Object plugins:
{ apply: (ctx, config) => any, … }– Theapplymethod is invoked upon registration. - Class plugins:
class Foo { constructor(ctx, config) { … } }– Instantiated withnew Foo(ctx, config).
All three forms share common runtime metadata properties:
name?: string– Optional identifier for the plugin.Config?: StandardSchemaV1– Optional configuration schema for validation.inject?: Inject– Declares dependencies that Cordis auto-injects before execution.
Defining Plugins in Cordis
Functional Plugin Definition
The simplest approach defines a plugin as a function receiving Context and configuration:
// my-plugin.ts
import { Context } from '@cordisjs/core';
export default function LoggerPlugin(ctx: Context, config: { level: string }) {
ctx.logger.info(`Logger level set to ${config.level}`);
}
When registered, Cordis calls this function directly with the provided context and validated configuration.
Object Plugin with Configuration Schema
For complex plugins requiring validation, export an object with apply and a Config schema:
// timer-plugin.ts
import { Schema } from '@standard-schema/spec';
import { Context } from '@cordisjs/core';
export const TimerPlugin = {
name: 'timer',
Config: Schema.object({
interval: Schema.number().default(1000)
}),
apply(ctx: Context, config: { interval: number }) {
const timer = setInterval(() => ctx.logger.info('tick'), config.interval);
return () => clearInterval(timer); // cleanup function
},
};
The apply method executes upon registration, and the returned function handles disposal.
Class Plugin with Dependency Injection
Class plugins support decorators for automatic service injection:
// service-plugin.ts
import { Context, Inject } from '@cordisjs/core';
export class ServicePlugin {
static init = Symbol('init'); // service lifecycle marker
constructor(public ctx: Context) {}
@Inject('logger')
init(logger) {
logger.info('ServicePlugin started');
return () => logger.info('ServicePlugin stopped');
}
}
The @Inject decorator declares dependencies that Cordis resolves before calling the init method.
Registering Plugins in Cordis
The Registration API
The Context interface exposes two primary methods defined in packages/core/src/context.ts:
declare module './context' {
export interface Context {
/** Register a plugin */
plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>
/** Register a dependency‑only plugin */
inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>
}
}
Calling ctx.plugin() performs three operations:
- Validation: Throws if the argument is not a function or an object with
apply. - Runtime creation: Instantiates the plugin once per
ContextviaRegistryService.plugin(). - Fiber spawning: Returns a
Fiberthat resolves when the plugin’s setup phase completes.
The RegistryService Resolution Flow
The core registration logic lives in packages/core/src/registry.ts:
- Resolution:
RegistryService.resolve()detects whether the plugin is a plain function or an object with anapplymethod. - Registration:
RegistryService.plugin()creates a runtime entry (Plugin.Runtime) and aFiberrepresenting the plugin’s lifecycle. - Injection: The system resolves declared dependencies before invoking the plugin logic.
Dependency-Only Registration
Use ctx.inject() to register logic that only consumes dependencies without defining a reusable plugin:
await ctx.inject(['logger'], (ctx) => {
ctx.logger.info('Injected logger ready');
});
This is a convenience wrapper that creates a lightweight fiber without the full plugin instantiation overhead.
Managing Plugin Lifecycles with Fibers
Every registered plugin creates a Fiber instance defined in packages/core/src/fiber.ts. Fibers implement PromiseLike<Fiber>, allowing you to await ctx.plugin(...) to ensure initialization completes before proceeding:
import LoggerPlugin from './my-plugin';
await ctx.plugin(LoggerPlugin, { level: 'debug' });
// Execution continues only after LoggerPlugin setup finishes
The Fiber class manages:
- Configuration storage: Holds the validated plugin configuration.
- Disposable tracking: Maintains a list of cleanup functions (event listeners, timers, services).
- Automatic disposal: Calling
dispose()removes all resources and cascades cleanup to child plugins.
Nested plugins create hierarchical fiber trees; disposing a parent fiber automatically cleans up all children.
Complete Cordis Plugin Examples
Basic Registration
import LoggerPlugin from './my-plugin';
await ctx.plugin(LoggerPlugin, { level: 'debug' });
Object Plugin with Schema
await ctx.plugin(TimerPlugin, { interval: 2000 });
Class Plugin Registration
await ctx.plugin(ServicePlugin);
Nested Plugins
Plugins can register other plugins, each receiving its own Fiber with cascaded cleanup:
await ctx.plugin(async (ctx) => {
ctx.logger.info('parent plugin');
await ctx.plugin((ctx) => ctx.logger.info('child plugin'));
});
Summary
- Cordis accepts three plugin shapes: functions, objects with
apply, and classes. - Registration occurs via
ctx.plugin(), which validates the plugin and returns a promise-likeFiber. RegistryServiceinpackages/core/src/registry.tshandles resolution, instantiation, and dependency injection.- Fibers manage the async lifecycle; awaiting
ctx.plugin()ensures setup completes before execution continues. - Cleanup is automatic through fiber disposal, which removes event listeners, services, and child plugins.
Frequently Asked Questions
What are the valid plugin shapes in Cordis?
Cordis supports function plugins (ctx, config) => any, object plugins with an apply method { apply: (ctx, config) => any }, and class plugins with a constructor constructor(ctx, config). All three are processed by RegistryService.resolve() in packages/core/src/registry.ts.
How does ctx.plugin() handle asynchronous initialization?
ctx.plugin() returns a Fiber that implements PromiseLike<Fiber>, as defined in packages/core/src/fiber.ts. Awaiting this fiber pauses execution until the plugin's setup phase completes, ensuring dependencies are ready before subsequent code runs.
What happens when a plugin is disposed?
When a fiber's dispose() method is called, Cordis executes all registered cleanup functions, removes event listeners, and disposes of any services created by the plugin. Child plugins registered within the disposed plugin are automatically cleaned up in cascade fashion.
Can I register a plugin without an apply method?
Yes. Use ctx.inject(deps, callback) to register dependency-only logic that executes after Cordis resolves the specified dependencies. This creates a lightweight fiber without requiring a full plugin definition with an apply function or configuration schema.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →