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): any method

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 with apply methods
  • The RegistryService.plugin method creates a Fiber for each registered plugin to manage execution context
  • The @Inject decorator enables declarative dependency injection by storing requirements on class metadata
  • The Include plugin in packages/include/src/index.ts supports externalized YAML/JSON configuration with hot-reloading capabilities
  • The EntryTree in packages/loader/src/index.ts processes 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →