# How to Implement Hot Module Replacement (HMR) in Cordis

> Learn how to implement Hot Module Replacement HMR in Cordis using the official @cordisjs/plugin-hmr. Reload only changed code and preserve app state for faster development.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: how-to-guide
- Published: 2026-08-24

---

**Cordis provides an official `@cordisjs/plugin-hmr` package that enables hot module replacement by watching plugin files, analyzing dependency graphs to determine affected plugins, and reloading only changed code while preserving application state, with automatic fallback to full process restart when core framework files change.**

The Cordis framework (maintained in the `cordiverse/cordis` repository) ships with a sophisticated HMR system that eliminates the need for full server restarts during development. By leveraging the `@cordisjs/plugin-hmr` package, developers can implement hot module replacement that intelligently reloads only modified plugins while maintaining the integrity of the dependency tree and application context.

## How the Cordis HMR Plugin Works

The core implementation resides in [`packages/hmr/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/index.ts), where the `Hmr` class extends the base `Service` to provide dependency-aware reloading capabilities. The system operates through a structured process that balances granular updates with safety guarantees.

### Service Registration and Context Integration

The `Hmr` class is injected with the **loader** and **timer** services and registered on the `Context` as `ctx.hmr`. In [`packages/hmr/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/index.ts) at lines 49-55, the service establishes itself within the Cordis dependency injection container, making HMR capabilities available throughout the application lifecycle.

### Module Resolution and File Watching

Before watching begins, the `_resolve` method at lines 90-95 abstracts internal loader versions (`v1` and `v2`) to normalize module URLs for subsequent cache clearing. The service then initializes a **chokidar** watcher at lines 108-115, configured with user-provided `root` directories and `ignored` patterns. Change events are automatically debounced using the configurable timer to prevent reload cascades during rapid file edits.

### Dependency Analysis and Change Classification

When files change, they enter a `stashed` queue. The `analyzeChanges()` method at lines 27-74 recursively traverses the `ModuleJob` dependency graph via `loadDependencies` to classify changes into three categories:

- **Accepted**: Files that must be reloaded
- **Declined**: Files that should be ignored  
- **Externals**: Framework core files that trigger full process restart

The system detects externals by traversing the dependency graph of the CLI entry point at lines 122-155, ensuring that changes to Cordis internals trigger `loader.exit()` rather than attempting unsafe hot swaps.

### Selective Plugin Reloading

The `partialReload` logic at lines 42-73 builds a mapping from configuration tree URLs to plugin names, then determines which plugins have dependencies in the `accepted` set. Only those specific plugins are queued for reload, avoiding unnecessary disruption to unaffected parts of the application.

### Cache Management and Safe Re-importing

Before re-importing, the system clears both **ESM `loadCache`** and **CJS `require.cache`** for every accepted file at lines 90-99. The implementation uses `Map.prototype.delete` to ensure compatibility across Node.js versions 22-24, where internal cache structures differ.

The plugin re-imports each entry file using `ctx.loader.import` while preserving stack traces. If imports fail, `handleError` rolls back caches to their previous state at lines 120-130, ensuring that compilation errors never leave the application in a broken state.

### Runtime Reregistration and Events

For each successfully imported plugin, the old runtime is disposed via `ctx.registry.delete` and the new one is instantiated with preserved `runtime` objects, ensuring fibers maintain their original entry objects at lines 31-38.

After successful reloads, the plugin emits two events on the context:

- **`hmr/change`**: Fires for individual file changes
- **`hmr/reload`**: Provides a map of all reloaded plugins

Consumers can listen to these events to react to hot updates programmatically at lines 130-132.

## Configuring HMR in Your Cordis Project

Add the `@cordisjs/plugin-hmr` package to your [`cordis.yml`](https://github.com/cordiverse/cordis/blob/main/cordis.yml) configuration file:

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

```

The `root` field specifies directories to watch, while `ignored` excludes generated folders and dependencies. The `debounce` value controls throttling in milliseconds, batching rapid successive changes into single reload cycles.

## Practical Implementation Examples

### Basic Setup with Hot-Reloaded Plugins

This example demonstrates setting up a Cordis context with HMR enabled and loading a user plugin that supports hot reloading:

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

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

  // Load a user plugin defined in ./my-plugin.ts
  await ctx.loader.create({ 
    name: 'my-plugin', 
    config: { path: './my-plugin.ts' } 
  })

  // Listen to HMR events
  ctx.on('hmr/reload', (reloads) => {
    console.log('Reloaded plugins:', [...reloads.keys()].map(p => p.name))
  })
}
main()

```

After `ctx.loader.create` registers the plugin, any modifications to [`my-plugin.ts`](https://github.com/cordiverse/cordis/blob/main/my-plugin.ts) trigger hot reloading without process restart.

### Reacting to File-Level Changes

Monitor specific file modifications that don't trigger full plugin reloads:

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

```

This event fires for any file outside the external framework tree that doesn't require a full process restart.

### Querying the Dependency Graph

Use the `getLinked` helper to inspect dependencies of specific modules:

```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 of utils.ts:', linked)

```

This functionality is tested in [`packages/hmr/tests/index.spec.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/tests/index.spec.ts) at lines 84-92, demonstrating how to traverse the module graph for debugging or optimization purposes.

### Handling Errors During Hot Reloads

Implement graceful error handling to maintain application stability when hot reloads fail:

```typescript
import { handleError } from '@cordisjs/plugin-hmr'

try {
  await ctx.loader.import(pluginPath)
} catch (e) {
  handleError(ctx, e) // rolls back caches and logs the problem
}

```

This pattern ensures that syntax errors or runtime exceptions during module loading roll back to the previous stable state rather than crashing the application.

### Debouncing Rapid File Changes

The built-in debounce mechanism consolidates rapid edits into single reload cycles:

```typescript
// Simulate rapid edits to the same file
await ctx.hmr.watcher?.emit('change', 'src/plugin.ts')
await ctx.hmr.watcher?.emit('change', 'src/plugin.ts')
await ctx.hmr.watcher?.emit('change', 'src/plugin.ts')

```

With `debounce: 100`, these three events result in a single reload operation, optimizing performance during intense development sessions.

## Summary

- The `@cordisjs/plugin-hmr` package implements **dependency-aware hot module replacement** that reloads only affected plugins while preserving application state.

- The system uses **chokidar** for file watching with configurable debouncing, and recursively analyzes the `ModuleJob` graph to classify changes as accepted, declined, or external.

- **Cache clearing** targets both ESM and CJS module systems for Node.js 22-24 compatibility, with automatic rollback mechanisms that restore previous states when imports fail.

- Changes to **Cordis core files** (externals) trigger full process restarts via `loader.exit()`, while plugin changes use `ctx.registry.delete` and re-instantiation to maintain fiber contexts.

- Developers can listen to **`hmr/change`** and **`hmr/reload`** events to build custom tooling around the hot reload lifecycle.

## Frequently Asked Questions

### What is the difference between `hmr/change` and `hmr/reload` events?

The `hmr/change` event fires immediately when a file modification is detected, providing the file URL. The `hmr/reload` event fires after successful plugin reloading, providing a Map of all reloaded plugin instances. Use `hmr/change` for logging or preprocessing, and `hmr/reload` for updating dependent services or clearing derived caches.

### Why does changing certain files trigger a full restart instead of hot reloading?

When you modify files within the Cordis framework itself (classified as **externals**), the HMR system calls `loader.exit()` to perform a full process restart. This safety mechanism prevents instability that could result from hot-swapping core runtime components while maintaining active service references.

### How does Cordis HMR handle ESM and CJS module compatibility?

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` and CJS `require.cache` using `Map.prototype.delete` operations. This dual-cache approach ensures that module reloading works correctly across Node.js versions 22-24, regardless of whether your project uses ES modules or CommonJS.

### Can I exclude specific directories from triggering reloads?

Yes. Configure the `ignored` array in your HMR plugin configuration to exclude directories using glob patterns. Common exclusions include `'**/node_modules'`, `'**/.*'` for hidden files, and project-specific folders like `'cache'` or `'data'` to prevent unnecessary reloads when generated files change.