Skip to main content
CommandKit’s development server includes Hot Module Replacement (HMR) functionality that allows you to update commands and events without restarting your bot. This dramatically speeds up development by preserving bot state while applying code changes.

How HMR Works

CommandKit’s HMR system uses a watch-build-reload architecture:
  1. File Watcher - Monitors your src/ directory for changes
  2. Incremental Build - Rebuilds only changed files
  3. IPC Communication - Sends reload events to the running process
  4. Handler Reload - Dynamically reloads commands or events
Source: packages/commandkit/src/cli/development.ts

Starting Dev Server

commandkit dev
This starts the development server with:
  • File watching enabled
  • Hot reload for commands and events
  • Automatic rebuilds on changes
  • Manual reload commands
Source: packages/commandkit/src/cli/development.ts:55

HMR Event Types

HMREventType

enum HMREventType {
  ReloadCommands = 'reload-commands',
  ReloadEvents = 'reload-events',
  Unknown = 'unknown',
}
Source: packages/commandkit/src/utils/constants.ts (referenced in development.ts:12)

Event Flow

Development Commands

While the dev server is running, you can type these commands:

r - Full Restart

Kills the process and performs a complete restart.
r
Source: packages/commandkit/src/cli/development.ts:201

rc - Reload Commands

Reloads all commands without restarting.
rc
Source: packages/commandkit/src/cli/development.ts:207

re - Reload Events

Reloads all events without restarting.
re
Source: packages/commandkit/src/cli/development.ts:212

HMR Implementation

File Watching

The dev server uses Chokidar to watch files.
import { watch } from 'chokidar';

const watcher = watch([join(cwd, 'src'), ...configPaths], {
  ignoreInitial: true,
});

watcher.on('change', hmrHandler);
watcher.on('add', hmrHandler);
watcher.on('unlink', hmrHandler);
Source: packages/commandkit/src/cli/development.ts:62

Smart Detection

CommandKit automatically determines what to reload based on file path.
const isCommandSource = (p: string) =>
  p.replaceAll('\\', '/').includes('src/app/commands');

const isEventSource = (p: string) =>
  p.replaceAll('\\', '/').includes('src/app/events');
Source: packages/commandkit/src/cli/development.ts:41-49

HMR Handler

const performHMR = debounce(async (path?: string): Promise<boolean> => {
  if (!path || !ps) return false;

  let eventType: HMREventType | null = null;
  let eventDescription = '';

  if (isCommandSource(path)) {
    eventType = HMREventType.ReloadCommands;
    eventDescription = 'command(s)';
  } else if (isEventSource(path)) {
    eventType = HMREventType.ReloadEvents;
    eventDescription = 'event(s)';
  } else {
    eventType = HMREventType.Unknown;
    eventDescription = 'unknown source';
  }

  if (eventType) {
    console.log(`Attempting to reload ${eventDescription} at ${path}`);

    await buildAndStart(cwd, true);
    const hmrHandled = await sendHmrEvent(eventType, path);

    if (hmrHandled) {
      console.log(`Successfully hot reloaded ${eventDescription}`);
      return true;
    }
  }

  return false;
}, 700);
Source: packages/commandkit/src/cli/development.ts:134

IPC Communication

interface IpcMessageCommand {
  event: HMREventType;
  path: string;
  id?: string;
}
Source: packages/commandkit/src/utils/dev-hooks.ts:8

Sending HMR Events

const sendHmrEvent = async (
  event: HMREventType,
  path?: string,
): Promise<boolean> => {
  if (!ps || !ps.send) return false;

  const messageId = randomUUID();
  const messagePromise = waitForAcknowledgment(messageId);

  ps.send({ event, path, id: messageId });

  // Wait for acknowledgment or timeout after 3 seconds
  const res = await Promise.race([
    messagePromise,
    sleep(3000).then(() => {
      console.warn(`HMR acknowledgment timed out for event ${event}`);
      return false;
    }),
  ]);

  return res;
};
Source: packages/commandkit/src/cli/development.ts:95

Receiving HMR Events

Dev Hooks Registration

export function registerDevHooks(commandkit: CommandKit) {
  if (!COMMANDKIT_IS_DEV) return;

  process.on('message', async (message) => {
    if (typeof message !== 'object' || message === null) return;

    const { event, path, id } = message as IpcMessageCommand;
    if (!event) return;

    let accepted = false;
    let prevented = false;
    let handled = false;

    const hmrEvent: CommandKitHMREvent = {
      accept() { accepted = true; },
      preventDefault() { prevented = true; },
      path,
      event,
    };

    try {
      // Allow plugins to handle HMR
      await commandkit.plugins.execute(async (ctx, plugin) => {
        if (accepted) return;
        await plugin.performHMR?.(ctx, hmrEvent);
      });

      if (prevented) return;

      switch (event) {
        case HMREventType.ReloadCommands:
          commandkit.commandHandler.reloadCommands();
          handled = true;
          break;
        case HMREventType.ReloadEvents:
          commandkit.eventHandler.reloadEvents();
          handled = true;
          break;
      }
    } finally {
      // Send acknowledgment
      if (id && process.send) {
        process.send({
          type: 'commandkit-hmr-ack',
          id,
          handled: handled || accepted,
        });
      }
    }
  });
}
Source: packages/commandkit/src/utils/dev-hooks.ts:51

Plugin HMR Support

Plugins can intercept and customize HMR behavior.

CommandKitHMREvent

interface CommandKitHMREvent {
  event: HMREventType;
  path: string;
  
  // Indicates this event was handled by the plugin
  accept: () => void;
  
  // Prevents default CommandKit handling
  preventDefault: () => void;
}
Source: packages/commandkit/src/utils/dev-hooks.ts:26

Custom Plugin HMR

import { CommandKitPlugin } from 'commandkit';

export const customPlugin: CommandKitPlugin = {
  name: 'custom-hmr-plugin',
  
  async performHMR(ctx, event) {
    if (event.path.includes('custom-module')) {
      console.log('Reloading custom module...');
      
      // Custom reload logic
      await reloadCustomModule();
      
      // Prevent default handling
      event.accept();
    }
  },
};

Reload Handlers

Command Reload

async reloadCommands() {
  // Clear command cache
  this.commands.clear();
  
  // Rescan command directory
  await this.commandsRouter.scan();
  
  // Load commands again
  await this.loadCommands();
  
  console.log('Commands reloaded');
}
Source: Referenced in packages/commandkit/src/commandkit.ts:438

Event Reload

async reloadEvents() {
  // Remove existing event listeners
  this.events.forEach((event) => {
    this.client.removeListener(event.name, event.handler);
  });
  
  // Clear event cache
  this.events.clear();
  
  // Rescan event directory
  await this.eventsRouter.scan();
  
  // Register events again
  await this.loadEvents();
  
  console.log('Events reloaded');
}
Source: Referenced in packages/commandkit/src/commandkit.ts:445

Debugging HMR

Enable Debug Logging

COMMANDKIT_DEBUG_HMR=true commandkit dev
This logs all HMR events:
Received HMR event: reload-commands for src/app/commands/ping.ts
Attempting to reload command(s) at src/app/commands/ping.ts
Successfully hot reloaded command(s) at src/app/commands/ping.ts
Source: packages/commandkit/src/utils/dev-hooks.ts:60

HMR Failures

If HMR fails, CommandKit falls back to a full restart.
const hmrHandler = async (path: string) => {
  if (isConfigUpdate(path)) return;
  
  const hmr = await performHMR(path);
  if (hmr) return; // HMR succeeded
  
  console.log('⚡️ Performing full restart due to changes in', path);
  
  ps?.kill();
  ps = await buildAndStart(cwd);
};
Source: packages/commandkit/src/cli/development.ts:184

Limitations

What Can Be Hot Reloaded

Commands
  • Chat input handlers
  • Autocomplete handlers
  • Message commands
  • Context menu commands
Events
  • Discord.js event handlers
  • Custom event handlers

What Requires Full Restart

Configuration Changes
  • commandkit.config.ts modifications
  • Plugin additions/removals
  • Build settings
Core Changes
  • Middleware modifications
  • Global state changes
  • Client instance changes
Source: packages/commandkit/src/cli/development.ts:170

Performance

Debouncing

HMR uses a 700ms debounce to batch rapid file changes.
const performHMR = debounce(async (path?: string) => {
  // Reload logic
}, 700);
Source: packages/commandkit/src/cli/development.ts:134

Acknowledgment Timeout

HMR waits up to 3 seconds for acknowledgment before assuming failure.
await Promise.race([
  messagePromise,
  sleep(3000).then(() => {
    console.warn('HMR acknowledgment timed out');
    return false;
  }),
]);
Source: packages/commandkit/src/cli/development.ts:108

Best Practices

  1. Keep Handlers Pure: Avoid side effects in command/event handlers that might not be cleaned up during reload
  2. Use Local State: Store state in context or CommandKit store, not in module-level variables
  3. Clean Up Listeners: If you register custom event listeners, clean them up properly
  4. Test Full Restart: Occasionally test with a full restart to ensure everything works correctly
  5. Monitor Console: Watch for HMR success/failure messages during development

Example: HMR-Friendly Command

import { ChatInputCommand } from 'commandkit';

// ❌ Bad: Module-level state won't reset on HMR
let commandCount = 0;

export const chatInput: ChatInputCommand = async (ctx) => {
  commandCount++;
  await ctx.interaction.reply(`Called ${commandCount} times`);
};
import { ChatInputCommand } from 'commandkit';

// ✅ Good: Use context store for state
export const chatInput: ChatInputCommand = async (ctx) => {
  const store = ctx.commandkit.store;
  const count = (store.get('commandCount') || 0) + 1;
  store.set('commandCount', count);
  
  await ctx.interaction.reply(`Called ${count} times`);
};

Production Builds

HMR is automatically disabled in production builds.
if (!COMMANDKIT_IS_DEV) return;
Source: packages/commandkit/src/utils/dev-hooks.ts:52 Production mode:
  • No file watching
  • No IPC overhead
  • Optimized builds
  • No dev hooks registered

Build docs developers (and LLMs) love