# How Cordis HMR Clears ESM and CJS Module Caches for Reliable Hot Reloading

> Learn how Cordis HMR efficiently clears ESM and CJS module caches using Map.prototype.delete and require.cache for reliable hot reloading with automatic rollback.

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

---

**Cordis HMR clears both module systems by calling `Map.prototype.delete` on the internal ESM `loadCache` and deleting entries from `require.cache` for CommonJS, with automatic rollback if re-import fails.**

The Cordis framework's **Hot-Module-Replacement (HMR)** service handles simultaneous cache invalidation across JavaScript's two module systems. This dual-cache clearing mechanism ensures that neither stale ESM nor stale CJS modules persist after a hot reload, preventing the common pitfall where one module system returns outdated code while the other reflects updates.

## The Two Cache Systems Cordis Must Clear

Modern Node.js applications can mix **ES modules (ESM)** and **CommonJS (CJS)** in the same codebase. Cordis loads plugins via dynamic `import()`, which can resolve either format. When HMR triggers, both caches must synchronize—otherwise a deleted ESM entry could still have a living CJS counterpart that contaminates the reload.

### ESM Cache: The ModuleLoader `loadCache`

Node.js maintains an internal `loadCache` (a `Map` of `ModuleJob` objects) to track loaded ES modules. Cordis accesses this through the loader's internal API.

In [`packages/hmr/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/index.ts) at lines 94-98, the HMR service removes ESM cache entries:

```typescript
// Force clear ESM load cache using prototype method
// Works for Node 22/23 (plain Map) and Node 24 (Map subclass with type slots)
for (const url of this.accepted) {
  Map.prototype.delete.call(this.internal.loadCache, url)
}

```

The use of `Map.prototype.delete.call()` rather than direct `.delete()` is defensive: **Node 24 changed `loadCache` to a Map subclass where `.delete()` only clears a type slot** without removing the entry. The prototype method guarantees complete eviction across Node versions.

### CJS Cache: `Module._cache`

CommonJS modules are cached in `require.cache`, a plain object keyed by absolute file paths. Cordis handles this at lines 100-105 of the same file:

```typescript
// Clear classic require.cache for CJS modules
const path = fileURLToPath(url)
if (path in require.cache) {
  cjsBackup.set(path, require.cache[path])
  delete require.cache[path]
}

```

The `fileURLToPath` conversion is essential because ESM identifiers use `file:` URLs while `require.cache` uses filesystem paths.

## The Cache Clearing Orchestration

The clearing logic lives within **`Hmr.partialReload()`** (around line 74 in [`packages/hmr/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/index.ts)). The method follows a four-phase pattern:

1. **Collect accepted files** — gather modified files marked for reload in `this.accepted`
2. **Backup current entries** — store existing cache state in `esmBackup` and `cjsBackup` for potential rollback
3. **Execute cache deletion** — run the ESM and CJS clearing code shown above
4. **Attempt re-import** — load updated plugin entry files; if any throw, restore backups via the `rollback` closure

### Rollback Safety

Cache entries are backed up **before** deletion. If re-import fails—due to syntax errors, missing dependencies, or runtime exceptions—the HMR service restores the original cache state. This prevents a broken module from permanently corrupting the application state.

## Complete HMR Setup Example

Enable Cordis HMR in your application with minimal configuration:

```typescript
import { Context, hmr } from 'cordis'
import { createServer } from 'http'

const ctx = new Context({
  plugins: [hmr],
  hmr: {
    root: ['src'],
    ignored: ['**/node_modules', '**/.git'],
    debounce: 150,
  },
})

const server = createServer((req, res) => {
  ctx.emit('request', { req, res })
  res.end('Hello from Cordis!')
})

server.listen(3000, () => console.log('Listening on :3000'))

```

When files in `src/` change:

- **chokidar** detects filesystem events
- Dependency analysis classifies modules as accepted/declined
- Both `loadCache` and `require.cache` are cleared via the mechanisms above
- Plugin entries re-import with zero-downtime service updates

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`packages/hmr/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/index.ts) | Core HMR service with `partialReload()` and cache clearing logic |
| [`packages/hmr/src/error.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/error.ts) | Error handling utilities for failed reloads |
| [`packages/loader/src/internal.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/internal.ts) | `ModuleLoader` implementation exposing `loadCache` for manipulation |

## Summary

- **Cordis HMR clears ESM cache** via `Map.prototype.delete.call(this.internal.loadCache, url)` for Node version compatibility
- **Cordis HMR clears CJS cache** via `delete require.cache[fileURLToPath(url)]` using absolute path conversion
- **Both caches are cleared atomically** within `partialReload()` before attempting re-import
- **Backup and rollback** protect against failed reloads, restoring original cache entries if needed
- **Dual-cache synchronization** prevents stale module versions from cross-contaminating ESM and CJS boundaries

## Frequently Asked Questions

### Why does Cordis use `Map.prototype.delete.call()` instead of direct `.delete()`?

Node 24 introduced a Map subclass for `loadCache` where the native `.delete()` only clears a type slot without fully removing the entry. Calling the prototype method directly bypasses this subclass behavior and guarantees complete eviction on all supported Node versions.

### What happens if a hot reload fails after clearing caches?

The `partialReload()` method backs up `esmBackup` and `cjsBackup` before deletion. If any re-import throws, the rollback closure restores these backups, returning the application to its pre-reload state without permanent cache corruption.

### Does Cordis HMR work with mixed ESM/CJS codebases?

Yes. By design, Cordis clears both the ESM `loadCache` and CJS `require.cache` simultaneously. This prevents scenarios where a deleted ESM module could still resolve through a stale CJS cache entry or vice versa.

### Where is the `loadCache` exposed to the HMR service?

The cache lives inside `ModuleLoader` in [`packages/loader/src/internal.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/internal.ts). The HMR service receives a reference to `this.internal.loadCache` and manipulates it directly during the reload cycle.