# How Cordis ModuleLoader Manages Entry Trees and Config Loading

> Discover how Cordis ModuleLoader uses EntryTree to manage plugin hierarchies and load configurations efficiently via the isolate plugin and Node ESM loader.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: internals
- Published: 2026-08-23

---

**The Cordis `ModuleLoader` organizes plugins into a hierarchical entry tree using the abstract `EntryTree` class in [`packages/loader/src/config/tree.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/tree.ts), while loading configurations through the isolate plugin and delegating module resolution to Node's internal ESM loader.**

Cordis relies on a first-class `loader` service to handle dynamic plugin management and configuration loading. The `ModuleLoader` (exposed as `ctx.loader`) extends `EntryTree` to provide CRUD operations on entries, intercept configuration changes, and bridge Node's native module system. This article breaks down the implementation details from the `cordiverse/cordis` source code.

---

## Entry Tree Architecture in `EntryTree`

The entry tree forms the backbone of Cordis's plugin hierarchy. The abstract `EntryTree` class in [`packages/loader/src/config/tree.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/tree.ts) maintains two core data structures:

- **`this.root`** — an `EntryGroup` instance holding top-level entries
- **`this.store: Dict<Entry>`** — a flat map for O(1) entry lookup by unique ID

### Tree Traversal and Resolution

The `*entries()` generator yields all entries recursively, enabling full-tree operations. For ID-based navigation, the loader implements colon-separated path resolution:

| Method | Purpose |
|--------|---------|
| `resolve(id)` | Locates a concrete entry from a path like `a:b:c` |
| `resolveGroup(id)` | Returns the parent `EntryGroup` for a given ID (or root) |

### CRUD Operations

**`create(options, parent?, position?)`** inserts a new entry, updates the parent group, and persists the tree. Source: [`tree.ts`](https://github.com/cordiverse/cordis/blob/main/tree.ts) lines 76-81.

**`remove(id)`** deletes an entry by ID and cleans up its parent group reference.

**`update(id, options, parent?, position?)`** modifies or relocates an entry, then triggers a tree write. This powers runtime configuration updates. Source: [`tree.ts`](https://github.com/cordiverse/cordis/blob/main/tree.ts) lines 89-101.

**`import(name)`** handles dynamic module loading, delegating to either built-in modules or Node's internal loader.

---

## The `Loader` Service Implementation

The concrete `Loader` class in [`packages/loader/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts) extends `EntryTree` and wires Cordis's service ecosystem to the entry tree.

### Construction and Internal State

```typescript
public internal = ModuleLoader.fromInternal()   // Node's ESM loader wrapper
public builtins: Dict<any> = Object.create(null) // "cordis:" prefixed modules

```

The `fromInternal()` factory detects Node version and instantiates the appropriate loader implementation (v1 for Node 22-23, v2 for Node 24+).

### Event Hook Registration

The loader binds to three internal Cordis events during construction (lines 74-80, 88-124):

1. **`internal/update`** — Persists configuration changes to `entry.options` and writes the updated tree
2. **`internal/plugin`** — Tracks plugin fiber lifecycle, assigns `fiber.entry`, and logs loading/unloading
3. **`loader/config-update`** — Emitted after successful configuration refresh

These hooks keep the entry tree synchronized with runtime state changes.

### Configuration Interception with `Service.check`

The `Loader` implements `Service.check` (lines 33-37) to respect the `await` flag from `Loader.Intercept`. This delays disposal until pending tasks complete via `getTasks()`.

```typescript
// Pseudo-code from source
if (this.ctx.get('loader')?.await) {
  await Promise.all(this.getTasks())
}

```

### Dynamic Import Resolution

`Loader.import(name)` (lines 103-119) implements a two-tier resolution strategy:

```typescript
public async import(name: string) {
  // Built-in shortcut
  if (name.startsWith('cordis:')) {
    return this.builtins[name.slice(7)]
  }
  // Delegate to Node's internal loader
  return this.ctx.loader.internal.import(name)
}

```

This enables both user plugins (`import('my-plugin')`) and framework internals (`import('cordis:logger')`) to load through a unified interface.

---

## Node Internal Module Loader Wrapper

Cordis abstracts Node's evolving ESM loader API through `ModuleLoader` in [`packages/loader/src/internal.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/internal.ts). The version detection logic (lines 12-23) branches based on `process.versions.node`:

| Node Version | Loader Implementation |
|-------------|----------------------|
| 22.x - 23.x | `ModuleLoaderV1` |
| 24.x+ | `ModuleLoaderV2` |

Both implementations expose `import`, `resolve`, and `load` methods, allowing `Loader` to call `this.ctx.loader.internal.import()` without version-specific handling.

---

## Config Loading via the Isolate Plugin

Configuration files execute in isolated contexts through the **isolate plugin** registered at line 26 of [`index.ts`](https://github.com/cordiverse/cordis/blob/main/index.ts):

```typescript
ctx.plugin(isolate)

```

The isolate plugin ([`packages/loader/src/config/isolate.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/isolate.ts)) creates a sandboxed execution environment for each entry's configuration. This captures side-effects as structured `EntryOptions` and prevents configuration pollution across plugins.

The isolation mechanism ensures that:
- Configuration evaluation errors are contained per-entry
- Global modifications from one config don't affect others
- The loader can extract and persist `options.config` deterministically

---

## Complete Entry Lifecycle Flow

Understanding how components interact clarifies the design:

1. **Bootstrap**: `new Loader(ctx)` creates the root entry group and registers event listeners
2. **Plugin Discovery**: The isolate plugin evaluates a config file, producing `EntryOptions`
3. **Tree Insertion**: `loader.create(options, parent, position)` adds the entry to `this.store` and `parent.group`
4. **Runtime Monitoring**: The `internal/plugin` hook attaches the Cordis fiber to `entry.fiber`
5. **Hot Reload**: `internal/update` fires → `loader.update()` modifies `entry.options` → tree persists → `loader/config-update` emits

---

## Practical Code Examples

### Creating and Accessing Entries

```typescript
import { Context } from 'cordis'
import Loader from '@cordis/loader'

const ctx = new Context()
await ctx.start()

const loader = new Loader(ctx)  // registers as ctx.loader

const entry = await loader.create({
  name: 'data-processor',
  config: { batchSize: 100 }
}, null, 0)  // parent=null (root), position=0

console.log(entry.id)  // generated unique identifier

```

### Programmatic Configuration Update

```typescript
// Modify existing entry configuration
await loader.update('data-processor', {
  config: { batchSize: 500, retry: true }
})
// Triggers tree write and config-update event

```

### Loading Modules Through the Loader

```typescript
// User module via Node's internal loader
const processor = await loader.import('./processors/csv.js')

// Built-in module via cordis: prefix
const logger = await loader.import('cordis:logger')

```

---

## Summary

- **`EntryTree`** ([`packages/loader/src/config/tree.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/tree.ts)) provides the hierarchical storage and CRUD interface for plugin entries
- **`Loader`** ([`packages/loader/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts)) extends `EntryTree` with service integration, event hooks, and module resolution
- **`ModuleLoader`** ([`packages/loader/src/internal.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/internal.ts)) version-detects and wraps Node's native ESM loader
- **Isolate plugin** sandboxes configuration evaluation, producing structured entry options
- The **entry lifecycle** flows from config isolation → tree creation → fiber attachment → update synchronization

---

## Frequently Asked Questions

### How does Cordis resolve colon-separated entry IDs?

The `EntryTree.resolve()` method in [`packages/loader/src/config/tree.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/tree.ts) splits IDs like `a:b:c` and traverses nested `EntryGroup` instances. Each segment maps to a child entry or group, with the final segment resolving to a concrete `Entry` object. Missing segments throw resolution errors.

### What happens when a plugin configuration changes at runtime?

The `internal/update` event fires (line 74-80 in [`index.ts`](https://github.com/cordiverse/cordis/blob/main/index.ts)), triggering `loader.update()` to modify `entry.options.config`, persist the tree structure, and emit `loader/config-update`. The Service.check `await` flag can delay this until pending tasks finish.

### Why does Cordis wrap Node's internal ESM loader?

Node's ESM loader API changed between versions 22-23 (v1) and 24+ (v2). The `ModuleLoader` abstraction in [`packages/loader/src/internal.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/internal.ts) provides version detection and a unified interface, letting `Loader.import()` work across Node versions without conditional code.

### What is the purpose of the `cordis:` prefix in imports?

The `cordis:` prefix routes module requests to `loader.builtins` instead of the filesystem. This allows core services to expose stable interfaces while keeping the internal file structure flexible. Built-ins are registered in `Object.create(null)` to avoid prototype pollution.