Cordis Loader Config File Watching and Auto-Reload: A Complete Guide

Cordis provides a built-in HMR (Hot Module Reload) service that watches loader configuration files using Chokidar and automatically reloads plugins when changes are detected, supporting both partial reloads for cached modules and full process restarts for external dependencies.

The Cordis framework from cordiverse/cordis includes a sophisticated file watching system that monitors loader configurations like cordis.yml and triggers automatic plugin reloads during development. Understanding how this Cordis loader config file watching and auto-reload mechanism works is essential for building efficient development workflows that eliminate manual restarts.

How the HMR File Watcher Initializes

The HMR service creates a file system watcher that mirrors the loader's own configuration schema, ensuring that any changes to your setup files are immediately detected.

Creating the Chokidar Instance

In packages/hmr/src/index.ts, the service imports the watch function from Chokidar to instantiate an FSWatcher. The initialization occurs between lines 108-113:

import { watch } from 'chokidar';
// ...
this.watcher = watch(root, {
  ...this.config,
  cwd: this.baseDir,
  ignored: path => match(relative(this.baseDir, path)),
});

The root parameter defines which directories to monitor, while the ignored function filters out paths matching patterns like node_modules or hidden files. The cwd is set to this.baseDir, ensuring all paths are resolved relative to your project's base directory.

Configuring Watch Paths and Ignores

The HMR configuration interface extends ChokidarOptions and is validated using Schemastery. Defined in lines 81-99 of packages/hmr/src/index.ts, the schema specifies:

export interface Config extends ChokidarOptions {
  base?: string;
  root: string[];
  debounce: number;
  ignored: string[];
}
export const Config = z.object({
  base: z.string(),
  root: z.array(String).role('table').default(['.']),
  ignored: z.array(String).role('table').default([
    '**/node_modules', '**/.*', 'cache', 'data',
  ]),
  debounce: z.natural().role('ms').default(100),
});

By default, the watcher monitors the current directory (.) while ignoring node_modules, dotfiles, and cache or data directories. The debounce setting prevents excessive reloads by waiting 100ms after the last change event before triggering actions.

Handling File Changes and Reload Strategies

When the watcher detects a file modification, the service classifies the change and executes the appropriate reload strategy without manual intervention.

Classifying Change Events

The core logic resides in the 'change' event handler defined in packages/hmr/src/index.ts (lines 124-149). When a file changes, the service resolves the absolute path and converts it to a file URL for comparison:

this.watcher.on('change', async (path) => {
  const filename = resolve(this.baseDir, path);
  const url = pathToFileURL(filename).href;

  if (this.externals.has(url)) return loader.exit();               // full reload
  if (loader.internal!.loadCache.has(url)) {                       // partial reload
    this.stashed.add(url);
    return partialReload();
  }
  for (const entry of this.ctx.loader.entries()) {                 // config reload
    const include = entry.subtree as Include | undefined;
    if (include?.filename !== filename) continue;
    await include.refresh();
    return;
  }
  this.ctx.emit('hmr/change', url);
});

The handler distinguishes between three critical scenarios:

  • External framework files – Triggers loader.exit() for a full process restart
  • Cached module files – Queues a partialReload() after stashing the URL
  • Loader config files – Calls include.refresh() to reload the configuration in-place

Partial Reload vs. Full Restart

The partial reload mechanism clears specific entries from both the ESM loadCache and CJS require.cache before re-importing affected plugin entry files. This process maintains application state while updating only the changed code. If any step fails, the service rolls back changes to preserve process stability.

Conversely, external dependencies identified in this.externals require a complete process restart via loader.exit(), as they cannot be safely unlinked from the module graph during runtime.

Loader Configuration Schema Integration

Because the HMR service reuses the loader's configuration interface, changes to cordis.yml automatically trigger the config reload path. The include.refresh() method, implemented in packages/loader/src/config/entry.ts, re-parses the YAML configuration and updates plugin entries without restarting the entire application context.

This integration means that editing plugin paths, entry names, or other loader settings in your configuration file results in immediate, automatic reloading of the affected components.

Practical Implementation Examples

Basic Cordis App with HMR Enabled

Enable file watching by installing the HMR plugin and configuring the watcher roots:

import { Context } from 'cordis';
import loader from '@cordisjs/plugin-loader';
import hmr from '@cordisjs/plugin-hmr';

const ctx = new Context({
  loader: {
    // cordis.yml will be automatically watched
    config: 'cordis.yml',
  },
  // HMR options (optional – defaults are fine)
  hmr: {
    root: ['src'],
    ignored: ['node_modules', '.git'],
    debounce: 150,
  },
});

await ctx.plugin(loader);
await ctx.plugin(hmr);
await ctx.start();

This configuration watches the src directory and automatically reloads plugins whenever source files or the loader config (cordis.yml) changes.

Updating the Loader Config at Runtime

Consider this cordis.yml configuration:

loader:
  entries:
    - name: my-plugin
      path: ./plugins/my-plugin.ts

If you edit loader.entries[0].path to point at a different file and save, the HMR watcher detects the change, calls include.refresh(), and loads the new plugin module automatically without requiring a manual restart.

Manually Forcing a Reload

You can programmatically trigger the same reload logic used by the file watcher:

// Somewhere in your code
ctx.emit('hmr/reload', new Map()); // Triggers the same reload logic as a file change

Summary

  • Chokidar Integration: The HMR service in packages/hmr/src/index.ts uses Chokidar to create robust file system watchers with configurable ignore patterns and debouncing.
  • Triple Strategy: File changes trigger one of three paths—full restart for externals, partial reload for cached modules, or in-place refresh for loader configs.
  • Schema Alignment: The HMR configuration mirrors the loader's config schema, ensuring cordis.yml changes are detected and processed immediately.
  • Development Experience: Automatic reloading eliminates manual restarts during plugin development, with support for both JavaScript module cache clearing and YAML config re-parsing.

Frequently Asked Questions

How does Cordis detect changes in cordis.yml?

Cordis detects changes through the HMR service's file watcher, which monitors paths specified in the root configuration array. When cordis.yml is modified, the change event handler in packages/hmr/src/index.ts iterates through this.ctx.loader.entries() to find matching Include objects and calls include.refresh() to reload the configuration without restarting the process.

What is the difference between partial and full reloads in Cordis?

Partial reloads clear specific entries from the ESM loadCache and CJS require.cache before re-importing changed plugin files, preserving the application state. Full reloads occur when external framework files change, triggering loader.exit() to restart the entire process because these dependencies cannot be safely removed from the module cache at runtime.

Can I customize which files trigger a reload?

Yes, through the ignored array in the HMR configuration schema. By default, the watcher ignores **/node_modules, **/.*, cache, and data directories. You can customize this in your Context configuration under the hmr key to add or remove patterns according to your project structure.

How do I enable file watching in a Cordis application?

Install the @cordisjs/plugin-hmr package and register it with your Context instance after the loader plugin. The watcher starts automatically when you call ctx.start(), monitoring the directories specified in the root configuration option and using the debounce setting to control reload timing.

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 →