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

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 at lines 94-98, the HMR service removes ESM cache entries:

// 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:

// 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). 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:

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 Core HMR service with partialReload() and cache clearing logic
packages/hmr/src/error.ts Error handling utilities for failed reloads
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. The HMR service receives a reference to this.internal.loadCache and manipulates it directly during the reload cycle.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →