Patterns for Building Custom Cordis Plugins: Functions, Classes, and Dependency Injection
Cordis supports three primary plugin patterns—functions, class constructors, and objects with an apply method—each registered via ctx.plugin() and executed within isolated Fibers that handle dependency injection and lifecycle management.
Cordis is a modular, extensible framework designed for building composable applications through plugins. Understanding the patterns for building custom Cordis plugins enables developers to leverage its sophisticated dependency injection system and hierarchical loading capabilities. This guide explores the core architecture defined in cordiverse/cordis and provides practical implementations based on the actual source code.
Understanding Cordis Plugin Types
The Plugin Interface in registry.ts
In packages/core/src/registry.ts, Cordis defines the Plugin<T> type as a union of three distinct patterns. Each variant extends Plugin.Base, which accommodates metadata including plugin names, configuration schemas, and injection requirements.
The registry recognizes these implementation patterns:
- Plugin.Function:
(ctx: Context, config: T) => any - Plugin.Constructor:
new (ctx: Context, config: T) => any - Plugin.Object: An object literal containing an
apply(ctx: Context, config: T): anymethod
When you invoke ctx.plugin(plugin, config), the RegistryService.plugin method inspects the provided callback, creates a Fiber (a lightweight execution context that holds runtime data), and returns a promise-like object that resolves when initialization completes.
Implementing Custom Cordis Plugins
Function-Based Plugins
The simplest pattern uses a standard function receiving the Cordis Context and an optional configuration object. This approach suits lightweight utilities that register event listeners or simple commands.
Define the plugin in a dedicated file:
// src/plugins/hello.ts
import { Plugin } from 'cordis'
export const hello: Plugin.Function = (ctx, config: { greeting: string }) => {
ctx.on('greet', () => console.log(config.greeting))
}
Register it through the Context:
import { Context } from 'cordis'
import { hello } from './plugins/hello'
const ctx = new Context()
await ctx.plugin(hello, { greeting: 'Hello, Cordis!' })
Class-Based Plugins with Dependency Injection
For complex plugins requiring external services, use the @Inject decorator defined in packages/core/src/registry.ts. The decorator stores required service keys on the class metadata. During Fiber instantiation, Inject.resolve builds a dependency map and injects resolved services before invoking your plugin.
// src/plugins/logger.ts
import { Inject } from 'cordis'
@Inject('logger')
export class LoggerPlugin {
apply(ctx) {
ctx.logger.info('LoggerPlugin activated')
}
}
Register the instantiated class:
await ctx.plugin(new LoggerPlugin())
Object Plugins with Apply Methods
When you need a reusable, named plugin without class overhead, export an object implementing the apply method. This pattern offers explicit identification while maintaining the same execution signature as function plugins.
// src/plugins/timer.ts
export const timer = {
name: 'timer',
apply(ctx) {
ctx.on('tick', () => console.log('tick'))
}
}
Register the object directly:
await ctx.plugin(timer)
Dynamic Configuration and Loading
Loading Plugins from External Files
The built-in include plugin, implemented in packages/include/src/index.ts, enables declarative management through JSON or YAML files. This approach supports hot-reloading and externalized configuration without code changes.
Create a configuration file:
# plugins.yml
- id: hello
name: hello
apply: ./plugins/hello.ts
config:
greeting: "Hi from YAML"
Load it through the Context:
import { Include } from '@cordisjs/plugin-include'
await ctx.plugin(Include, { path: './plugins.yml' })
The Include plugin parses the file, builds an EntryTree via packages/loader/src/index.ts, and processes each entry through EntryTree.update, which ultimately calls ctx.plugin while preserving hierarchical loading order.
Hot-Reloading with Patches
Cordis supports runtime configuration modifications through patches. The applyPatches method in packages/include/src/index.ts inserts or modifies entries within the existing tree, enabling updates without application restarts.
Define a patch configuration:
# patches.yml
patches:
- id: hello
insert:
- name: extra
apply: ./plugins/extra.ts
When processed by the Include plugin, the patch merges new entries into the target group dynamically.
Plugin Lifecycle and Fiber Architecture
Registration and Initialization
Every plugin registration follows a strict four-phase lifecycle defined in the core system. First, Registration adds the plugin to the internal registry via ctx.plugin(). Second, Fiber Creation instantiates an isolated execution context that injects declared dependencies through the @Inject system. Third, Initialization executes the plugin's body—whether function invocation, constructor execution, or apply method dispatch. Finally, the Fiber maintains the runtime state until disposal.
The Fiber abstraction ensures that plugins operate within isolated scopes while maintaining access to the shared Context and its services.
Disposal and Cleanup
When removing a plugin via registry.delete(plugin), Cordis automatically disposes all associated Fibers. This mechanism guarantees that event listeners, timers, and other resources allocated during initialization are properly released, preventing memory leaks in long-running applications.
Summary
- Cordis recognizes three plugin patterns in
packages/core/src/registry.ts: functions, constructors, and objects withapplymethods - The
RegistryService.pluginmethod creates a Fiber for each registered plugin to manage execution context - The
@Injectdecorator enables declarative dependency injection by storing requirements on class metadata - The
Includeplugin inpackages/include/src/index.tssupports externalized YAML/JSON configuration with hot-reloading capabilities - The
EntryTreeinpackages/loader/src/index.tsprocesses configuration entries while preserving hierarchical loading order - Plugin removal triggers automatic disposal of Fibers and cleanup of associated resources
Frequently Asked Questions
What file contains the core plugin type definitions in Cordis?
The Plugin type union and Plugin.Base interface are defined in packages/core/src/registry.ts. This file also contains the RegistryService class with the plugin method that handles registration and the @Inject decorator used for dependency injection.
How do I declare dependencies in a class-based Cordis plugin?
Apply the @Inject decorator to your class, passing the required service keys as arguments. The decorator stores these requirements on the class metadata, and the Fiber constructor resolves them during instantiation. For example: @Inject('logger') before class MyPlugin injects the logger service when the plugin initializes.
Can I load Cordis plugins from external configuration files?
Yes. Use the Include plugin from packages/include/src/index.ts to load JSON or YAML files. The plugin parses the configuration, constructs an EntryTree, and registers each entry automatically. This supports hot-reloading and allows you to manage plugin configurations without modifying source code.
What happens when a plugin is removed from the registry?
Calling registry.delete(plugin) triggers the disposal phase, where Cordis destroys all Fibers associated with that plugin. This process cleans up event listeners, timers, and other resources allocated during initialization, ensuring proper memory management and preventing resource leaks.
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 →