# How Hot Module Replacement (HMR) is Implemented in Cordis: A Deep Dive into cordis-hmr

> Discover how cordis-hmr implements Hot Module Replacement. Learn about its dependency watcher, state preservation, and fallback to full restarts for efficient plugin reloads.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: deep-dive
- Published: 2026-09-12

---

**Cordis implements HMR through the `@cordisjs/plugin-hmr` package, which uses a dependency-aware watcher to selectively reload plugins while preserving application state and falling back to full restarts for core framework changes.**

The **Hot Module Replacement** system in Cordis allows developers to update plugin code without restarting the entire application. According to the cordiverse/cordis source code, the official HMR plugin watches file changes and performs intelligent dependency analysis to determine exactly which modules need reloading. This implementation lives primarily in [`packages/hmr/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/index.ts) and integrates deeply with the Cordis loader service.

## Core Architecture and Service Registration

The HMR system is built as a first-class service that interacts with Cordis's dependency injection container. Understanding how it registers itself and manages internal state reveals how the framework maintains stability during hot updates.

### Service Registration and Context Integration

In [`packages/hmr/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/index.ts), the `Hmr` class extends the base `Service` class and declares dependencies on the `loader` and `timer` services. When instantiated, it registers itself on the context as `ctx.hmr`, making its API available throughout the application lifecycle.

The constructor initializes internal maps for tracking file changes and stashed updates:

```typescript
// From packages/hmr/src/index.ts#L49-L55
class Hmr extends Service {
  static inject = ['loader', 'timer']
  
  constructor(ctx: Context, public config: Hmr.Config) {
    super(ctx, 'hmr')
    this.stashed = new Set<string>()
    // ...
  }
}

```

### Module Resolution and Cache Abstraction

Before watching can begin, the plugin must resolve module URLs in a version-agnostic way. The `_resolve` method in `packages/hmr/src/index.ts#L90-L95` abstracts differences between loader API versions (`v1` and `v2`), ensuring that the HMR plugin can clear the correct cache entry regardless of the underlying loader implementation.

This abstraction is critical because Cordis supports both ESM and CommonJS module systems. The resolution logic returns a file URL that can be used to purge entries from both the ESM `loadCache` and the CJS `require.cache`.

## The Hot Reload Lifecycle

When a developer saves a file, the HMR system executes a ten-step process to determine whether to reload a specific plugin, ignore the change, or restart the entire process.

### File Watching and Debouncing

Upon initialization, the service creates a **chokidar** watcher using the user-provided `root` directories and `ignored` patterns. The watcher is configured with a debounce timer to batch rapid successive changes:

```typescript
// Conceptual usage based on packages/hmr/src/index.ts#L108-L115
await ctx.plugin(Hmr, {
  root: ['.'],
  debounce: 50,  // milliseconds
  ignored: ['**/node_modules', '**/.*']
})

```

The debounce mechanism prevents a cascade of reloads when multiple files change simultaneously or when an editor performs rapid save operations.

### Change Detection and Classification

When a file change event fires, the path is added to an internal `stashed` set. The `analyzeChanges()` method (located at `packages/hmr/src/index.ts#L74-L27`) then traverses the dependency graph to classify each changed file into one of three categories:

- **Accepted**: Files that can be safely reloaded
- **Declined**: Files that should be ignored (not part of any plugin)
- **Externals**: Framework core files that require a full process restart

### Dependency Graph Analysis

The plugin builds a bidirectional understanding of module relationships. By walking the `ModuleJob` graph via `loadDependencies`, the `partialReload` method (`packages/hmr/src/index.ts#L42-L73`) constructs a map from config tree URLs to plugin names. It then checks whether any dependencies of a given plugin fall into the `accepted` set.

Only plugins with at least one dependency in the `accepted` set are queued for reloading. This **dependency-aware analysis** ensures that changing a utility file shared by three plugins triggers updates for all three, while changing an unused file triggers nothing.

### Selective Plugin Reloading

For each plugin marked for reload, the system performs the following sequence:

1. **Cache Clearing**: Removes entries from both ESM `loadCache` and CJS `require.cache` using `Map.prototype.delete` to handle differences across Node.js versions 22-24 (`packages/hmr/src/index.ts#L90-L99`).

2. **Re-importing**: Re-imports the entry file via `ctx.loader.import` while preserving the outer stack trace (`packages/hmr/src/index.ts#L120-L30`).

3. **Runtime Replacement**: Disposes the old plugin runtime using `ctx.registry.delete` and instantiates the new one while preserving the original `runtime` object so that fibers maintain their entry references (`packages/hmr/src/index.ts#L31-L38`).

### Error Handling and Rollback

If any import throws during the reload phase, the `handleError` utility rolls back the caches to their previous state. This **graceful rollback** mechanism guarantees that a syntax error in a hot update never leaves the application in a broken state—the previous working version remains active while the error is logged for the developer to fix.

After successful completion, the plugin emits two events:

- `hmr/change`: Fired for individual file changes
- `hmr/reload`: Contains a map of all reloaded plugins

## Configuration and Practical Usage

To enable HMR in a Cordis project, add the plugin to your configuration file:

```yaml

# cordis.yml

- id: hmr
  name: '@cordisjs/plugin-hmr'
  config:
    root:
      - .
    debounce: 50
    ignored:
      - '**/node_modules'
      - '**/.*'
      - 'cache'
      - 'data'

```

### Programmatic Setup Example

For programmatic control, instantiate the plugin directly:

```typescript
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Hmr from '@cordisjs/plugin-hmr'

async function main() {
  const ctx = new Context()
  await ctx.plugin(Loader)
  await ctx.plugin(Hmr, {
    root: ['.'],
    debounce: 100,
    ignored: ['**/node_modules']
  })

  // Register a plugin to be hot-reloaded
  await ctx.loader.create({ 
    name: 'my-plugin', 
    config: { path: './my-plugin.ts' } 
  })
  
  // Listen for reload events
  ctx.on('hmr/reload', (reloads) => {
    console.log('Reloaded:', [...reloads.keys()].map(p => p.name))
  })
}

```

### Reacting to File Changes

Monitor specific file updates without triggering plugin reloads:

```typescript
ctx.on('hmr/change', (url) => {
  console.log('File changed:', new URL(url).pathname)
})

```

### Querying the Dependency Graph

Inspect dependencies for debugging purposes:

```typescript
import { pathToFileURL } from 'url'
import { resolve } from 'path'

const url = pathToFileURL(resolve('src/utils.ts')).href
const linked = await ctx.hmr.getLinked(url)
console.log('Dependencies:', linked)

```

This corresponds to the test implementation found in `packages/hmr/tests/index.spec.ts#L84-L92`.

## Summary

- **Service-based architecture**: The `Hmr` class extends `Service` and registers as `ctx.hmr` in [`packages/hmr/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/index.ts), integrating with Cordis's `loader` and `timer` services.
- **Intelligent change classification**: The `analyzeChanges()` method separates files into accepted, declined, and external categories, triggering full restarts only for framework core changes.
- **Dependency-aware reloading**: By traversing the `ModuleJob` graph, the system reloads only plugins affected by changed dependencies, not the entire application.
- **Cross-platform cache management**: Uses `Map.prototype.delete` to safely clear both ESM and CJS caches across Node.js versions 22-24.
- **Atomic error handling**: Failed imports trigger automatic cache rollback via `handleError`, ensuring the application never remains in a broken state.
- **Event-driven feedback**: Emits `hmr/change` and `hmr/reload` events for monitoring and logging hot update cycles.

## Frequently Asked Questions

### How does cordis-hmr handle changes to node_modules?

Files matching the `ignored` patterns (which default to `**/node_modules`) are excluded from the watcher. If a dependency outside the watched tree changes, it will not trigger a hot reload unless it is explicitly included in the `root` configuration. However, if a core framework file within `node_modules` is part of the Cordis externals graph, changing it will trigger a full process restart via `loader.exit()`.

### What happens if a hot reload fails due to a syntax error?

The system implements graceful rollback. When `ctx.loader.import` throws during the re-import phase, the `handleError` utility restores the original ESM `loadCache` and CJS `require.cache` entries to their pre-reload state. The application continues running the previous working version of the plugin, and the error is logged for the developer to fix.

### Can I adjust how long the watcher waits before reloading?

Yes. The `debounce` configuration option controls the throttling window in milliseconds. With a setting of `debounce: 100`, rapid successive changes to the same file within 100 milliseconds are batched into a single reload event. This is implemented in `packages/hmr/src/index.ts#L108-L115` using the `timer` service.

### Does cordis-hmr work with both ESM and CommonJS modules?

Yes. The implementation in [`packages/hmr/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/index.ts) clears both the ESM `loadCache` (used by the Cordis loader) and the Node.js `require.cache` (used by CommonJS). The `_resolve` method abstracts loader version differences, ensuring that module URLs are correctly mapped for cache eviction regardless of the module system used by your plugins.