How Cordis Handles Configuration Merging Across Plugins
Cordis merges configuration across plugins by treating each plugin’s configuration as a service-level object that combines three distinct sources—intercept chain, base config, and head config—using a custom static Config.merge method when available, or falling back to shallow Object.assign merging.
Cordis treats plugin configuration as a first-class service primitive within the cordiverse/cordis ecosystem. When multiple plugins contribute configuration fragments, the framework resolves these into a single coherent configuration object through a deterministic merging pipeline implemented in the core service layer.
The Three Sources of Configuration
Cordis collects configuration from three distinct sources during the resolution process:
- Intercept chain – Intermediate configuration objects collected while traversing the
Context.interceptprototype chain that other plugins may augment - Base config – The default configuration passed when the service is constructed, serving as the starting point for the merge
- Head config – The configuration object supplied by the caller of
ctx.plugin(or similar), providing final overrides
These three layers ensure that plugins can both provide defaults and allow consumers to override them while respecting the interceptor pattern.
The resolveConfig Implementation
In packages/core/src/service.ts, the Service.resolveConfig method implements the core merging logic:
// packages/core/src/service.ts#L51-L66
if (this['Config']?.merge) {
return this['Config'].merge(...configs) // custom merge logic
} else {
return Object.assign({}, ...configs) // shallow merge fallback
}
This implementation checks for a static merge method on the service’s Config class. If present, Cordis delegates merging to the plugin’s custom implementation. Otherwise, it performs a shallow merge using Object.assign, spreading all collected configurations into a new object.
Custom Merge Strategies
Plugins requiring sophisticated merging semantics—such as deep merging, array deduplication, or validation—can implement a static Config.merge method. This method receives all configuration fragments as arguments and returns the final merged object.
import { Service } from 'cordis'
export class MyPlugin extends Service {
static Config = class {
static merge(...configs: any[]) {
const result = {}
for (const cfg of configs) {
for (const [k, v] of Object.entries(cfg)) {
if (Array.isArray(v)) result[k] = [...(result[k] ?? []), ...v]
else if (typeof v === 'object' && v !== null) result[k] = { ...(result[k] ?? {}), ...v }
else result[k] = v
}
}
return result
}
}
}
The custom merge strategy above handles array concatenation and deep object merging, contrasting with the default shallow behavior.
Persistent Configuration with the Include Plugin
The Include plugin extends Cordis configuration merging to persistent storage. It maintains a journal to record tree-side changes and reconciles these with external file modifications.
In packages/include/src/journal.ts, the merge function reconciles journal entries:
// packages/include/src/journal.ts#L45-L66
export function merge(journal: Journal, id: string, record: JournalRecord) {
const merged = mergeRecords(journal.get(id), record)
if (merged) {
journal.set(id, merged)
return
}
if (record.kind === 'remove') journal.delete(id)
}
The mergeRecords function combines older records with newer ones while preserving creation flags and handling upserts versus removals. After reconciliation, the Include service writes the merged configuration back to disk atomically, ensuring consistency between runtime edits and external file changes.
Configuration Resolution Flow
In practice, Cordis configuration merging follows this sequence:
- Plugin registration – Plugins expose a
Configclass with an optional staticmergemethod - Config resolution –
Service.resolveConfigcollects all fragments and invokes custom merge or defaults - Persistence – The Include plugin records changes in the journal, reconciles with file contents, and writes merged state back to disk
This pipeline guarantees deterministic configuration composition across plugin boundaries while supporting extensible merging semantics.
Summary
- Cordis configuration merging operates on three layers: intercept chain, base config, and head config
Service.resolveConfiginpackages/core/src/service.tsorchestrates the merge, preferring customConfig.mergeimplementations over shallowObject.assign- Plugins can implement sophisticated merging logic—including deep merges and array handling—via static
Config.mergemethods - The Include plugin persists configurations using a journaling system in
packages/include/src/journal.tsthat reconciles runtime changes with file edits - All merges are deterministic and respect the prototype chain order of the Cordis context
Frequently Asked Questions
How does Cordis choose between custom merge and default merge?
Cordis checks for the presence of a static merge method on the plugin's Config class via this['Config']?.merge. If the method exists, Cordis passes all configuration objects to it; otherwise, it falls back to Object.assign({}, ...configs) for a shallow merge.
Can multiple plugins modify the same configuration keys?
Yes. Cordis accumulates configuration contributions from the intercept chain, allowing multiple plugins to provide values for the same keys. The final head config supplied to ctx.plugin takes precedence in the merge order unless a custom merge method implements different conflict resolution logic.
What happens when configuration changes at runtime?
The Include plugin captures runtime changes through its journaling mechanism. When ctx.include('config.yml').commit() is called, it creates a journal entry. The merge function in journal.ts reconciles these entries with the current file state before writing the merged result back to disk.
Is deep merging supported by default?
No. The default behavior in packages/core/src/service.ts uses Object.assign, which performs shallow merging. Plugins requiring deep merging must implement a custom static Config.merge method that handles nested object recursion and array concatenation according to their specific domain requirements.
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 →