Best Practices for Cordis Plugin Entry Points: Configuration, Lifecycle, and Grouping
Cordis plugins are instantiated through lightweight entries—metadata wrappers defined in packages/loader/src/config/entry.ts that manage configuration interpolation, hierarchical IDs, and lifecycle transitions via the init, update, and refresh methods.
Cordis is a modular plugin framework where every plugin registers through the entry system located in the @cordis/loader package. Mastering best practices for Cordis plugin entry points ensures deterministic namespacing, clean configuration management, and predictable lifecycle behavior across your application.
Understanding the Entry Class Architecture
The Entry class serves as the central abstraction for plugin registration. It handles module resolution, context isolation, and state management through a declarative options interface.
EntryOptions Interface Structure
Every entry is defined by an EntryOptions object declared at lines 8-15 of packages/loader/src/config/entry.ts. This interface requires:
id(required): Unique identifier within the plugin treename(required): Module name to import (package or file path)config(optional): Configuration object supporting template interpolationgroup(optional): Boolean indicating whether the entry contains childrendisabled(optional): Boolean controlling runtime availabilityinject(optional): Dependency injection configuration
The id field acts as the canonical reference for lookups, while name specifies the actual module to load via the internal tree.import(name, getOuterStack) method.
Hierarchical ID Namespacing
When an entry belongs to a group, Cordis automatically prefixes its ID with the parent entry's ID using the pattern parent.id + '/' + id. This deterministic namespacing is implemented in the id getter at lines 56-62 of packages/loader/src/config/entry.ts.
This hierarchy enables clean lookup chains and prevents naming collisions across large plugin trees. Child entries inherit their parent's namespace, simplifying configuration scoping and cross-plugin communication.
The Disabled Flag Behavior
The disabled getter at lines 64-73 implements cascade logic: if any ancestor entry is disabled, all children are automatically disabled regardless of their individual flags. Groups themselves are always enabled; disabling a group entry effectively disables every descendant.
A disabled entry is never loaded, and Cordis automatically disposes the associated fibre when an entry transitions to disabled.
Lifecycle Management Best Practices
Cordis manages plugin state through three distinct lifecycle methods: init(), update(), and refresh(). Understanding these transitions prevents memory leaks and ensures clean reconfiguration.
The Init-Update-Refresh Flow
The async flow defined at lines 94-134 handles plugin instantiation and mutation:
init(): Imports the module viatree.import(), creates the runtime fibre, registers the plugin with its interpolated configuration, and emits theloader/entry-initeventupdate(): Mutates the entry's options, disposes the old fibre when necessary, emitsloader/partial-dispose, and optionally re-initialises the plugin if configuration changes require a full reloadrefresh(): A convenience method that only re-initialises a non-loaded, enabled entry
Always use entry.update(newOptions) instead of mutating entry.options directly. The update method guarantees proper disposal sequencing and event emission.
Event Hooks and Cleanup
The entry system emits two critical lifecycle events during construction and updates:
loader/entry-init: Emitted at line 49 when the entry first initialisesloader/partial-dispose: Emitted duringupdate()when replacing an already-loaded entry
Plugins can listen to these events to perform custom cleanup or state migration during hot-reconfiguration scenarios.
Configuration Patterns
Cordis supports declarative configuration through string interpolation and group-based scoping.
Context-Aware Configuration Interpolation
When a plugin's default export is not an entry group, the entry's config object undergoes interpolation through the current context. This process, handled by _resolveConfig and the interpolate method at lines 79-82 of packages/loader/src/config/entry.ts, replaces patterns like ${env.VAR} with actual environment values.
Store raw configuration templates in options.config and let Cordis resolve variables at load time. This keeps configuration portable across environments without hardcoding secrets.
Plugin Groups and Deterministic Namespacing
Set group: true in a parent entry's options to create a namespace container. Child entries automatically receive hierarchical IDs (parent.id/child.id), simplifying tree navigation and configuration inheritance.
Groups provide logical organization for related plugins while maintaining strict isolation between different branches of the plugin tree.
Runtime State Management
Effective entry point management requires atomic state changes and proper error handling.
Atomic Updates via entry.update()
Never mutate entry properties directly. Calling entry.update() ensures:
- Proper fibre disposal and cleanup
- Emission of
loader/partial-disposeevents - Conditional re-initialisation based on changed properties
- Thread-safe state transitions
Feature Toggling with the Disabled Flag
Leverage the disabled boolean for runtime feature flags without removing entries from the tree. When you disable an entry, Cordis automatically disposes the fibre and skips loading. Re-enabling triggers a fresh initialisation without requiring bootstrap script changes.
This pattern is particularly effective for A/B testing and conditional feature deployment.
Practical Implementation Examples
Defining a Simple Plugin Entry
Create a plugin that exports a default function accepting a Cordis Context:
import { Context } from 'cordis'
export default (ctx: Context) => {
ctx.on('ready', () => console.log('my-plugin is ready!'))
}
Register the plugin through the loader API exposed in packages/loader/src/index.ts:
await ctx.loader.entry('my-plugin', {
id: 'my-plugin',
name: 'my-plugin',
config: { greeting: '${env.GREETING}' },
disabled: false,
})
The name field matches the package identifier, while config uses interpolation for environment-specific values.
Creating a Plugin Group
Groups organize related plugins under a common namespace:
// Parent group entry
await ctx.loader.entry('utils', {
id: 'utils',
name: 'utils',
group: true,
})
// Child entry automatically receives ID 'utils/logger'
await ctx.loader.entry('utils/logger', {
id: 'logger',
name: '@my/logger',
config: { level: 'debug' },
})
The child entry's effective ID becomes utils/logger, and its configuration is scoped under the same tree branch.
Disabling a Plugin at Runtime
Toggle plugin availability without tree modification:
const entry = await ctx.loader.entry('feature-x', {
id: 'feature-x',
name: 'feature-x'
})
// Dispose fibre and stop plugin
await entry.update({ disabled: true })
// Re-enable later
await entry.update({ disabled: false })
Updating Configuration Without Full Reload
Modify configuration while preserving runtime state when possible:
await entry.update({
config: { greeting: 'Hello, world!' }
})
Cordis calls fiber.update with the new configuration if the entry is already loaded, avoiding unnecessary disposal cycles.
Summary
- Always provide stable, unique IDs: Use deterministic naming schemes and avoid changing IDs after creation to prevent lookup chain breaks.
- Use groups for organization: Set
group: trueto enable hierarchical namespacing and simplify configuration inheritance. - Leverage atomic updates: Call
entry.update()for all state changes to ensure proper lifecycle event emission and fibre management. - Implement feature toggles: Use the
disabledflag instead of removing entries to enable runtime plugin control. - Never import manually: Rely on
tree.import()through the entry system to ensure proper error handling and stack tracing via thegetOuterStackparameter. - Interpolate configuration: Store templates in
configfields and let Cordis resolve environment variables through context-aware interpolation.
Frequently Asked Questions
What is the difference between an entry's id and name fields?
The id field serves as the unique identifier within the plugin tree and supports hierarchical namespacing when entries belong to groups. The name field specifies the module to import—either a package name resolvable by Node.js or a file path. While id must be unique within the tree, name can be shared across multiple entries loading the same module with different configurations.
How does Cordis handle configuration environment variables?
Cordis interpolates configuration values through the entry's context when the plugin's default export is not an entry group. The _resolveConfig method processes templates like ${env.VARIABLE} at load time, replacing them with actual environment values. This occurs at lines 79-82 of packages/loader/src/config/entry.ts, keeping secrets out of configuration files while maintaining portability.
What happens when I disable a group entry?
When you set disabled: true on a group entry (or any of its ancestors), Cordis immediately disables every descendant entry regardless of their individual flags. The disabled getter at lines 64-73 implements this cascade logic. Disposing a group entry also disposes all child fibres, making this an effective nuclear option for shutting down feature sets.
Can I update a plugin's configuration without restarting the entire application?
Yes. Use entry.update({ config: newConfig }) to trigger a partial update. If the entry is already loaded, Cordis emits loader/partial-dispose and attempts to call fiber.update with the new configuration. This allows hot-reconfiguration for stateful plugins that support incremental updates without full re-initialisation.
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 →