Skip to content

Hot reload

Swap a saved file into your running bot with seedcord dev, and see which saves still need a full restart.

When you save a file under seedcord dev, seedcord unloads that file, imports the fresh copy, and registers its handlers again. Your bot keeps its connection to Discord the whole time, so you can switch back to Discord and test the change immediately.

What reloads in place

A saved file reloads when it holds one of these.

  • an interaction handler or middleware
  • a command
  • an event handler (gateway only)
  • a bus subscriber
  • files a plugin loads itself, if the plugin implements onHmr. Saving the plugin's own file asks for a restart. Plugins and hot reload has both

Any other file, like a card or a helper, reloads through the files that import it. A file that none of those groups imports has nothing to reload. Reloading works only in the development and test modes, so ENV=staging seedcord dev reloads nothing.

How it works

Node can't unload a module once it has imported it. A plain watcher like node --watch, nodemon, or tsx watch works around that by restarting the process, which for a gateway bot means logging in to Discord again, and Discord limits how many sessions (opens in a new tab) a bot may start per day. seedcord dev runs your bot inside Vite's module runner instead, which keeps its own graph of the modules it evaluated. seedcord removes the saved file from that graph and imports it again, inside the process that's already running.

The graph also records which modules imported which, so seedcord reloads the modules that imported your saved file, and the ones that imported those. Saving a card every handler builds therefore updates all of them. Vite's watcher reports each save, its transform step reads your TypeScript and your tsconfig paths, and its message channel carries what the CLI and your bot say to each other, like the restart prompt below.

A few files can't be swapped this way, because something read them once when the bot started.

Saves that need a restart

seedcord asks for a full restart when you save one of these, since the CLI or the bot reads each of them once at startup:

  • a file beside seedcord.config.ts named seedcord.config.*, tsconfig*.json, package.json, or a lockfile (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lock, bun.lockb)
  • a .env, .env.local, .env.<mode>, or .env.<mode>.local file beside seedcord.config.ts
  • on an edge bot, cloudflare.config.ts, .dev.vars, and .dev.vars.<mode>
  • the file instance points at
  • a file instance imports, directly or through another file, like a plugin your bot file attaches

These sit beside the config, so they count even when root is ./src. A package.json deeper in the project reloads like any other file. The build.start file isn't one of them, because seedcord dev never loads it.

After one of those saves, your bot keeps running the old code, and the terminal prints this.

Restart required. Press r to restart.

Nothing restarts automatically. Press r once you're ready to drop the connection for a moment.

Your own files can need a restart too. The usual case is a file that opens a connection or computes something once at import, since a reload imports it again and repeats that work. hmr.restart takes glob patterns. They're relative to root, so ./config/** and config/** match the same files. An absolute path works too.

seedcord.config.ts
import { defineConfig } from 'seedcord';

export default defineConfig({
    root: './src',
    instance: './bot.ts',
    build: { start: './index.ts' },
    hmr: {
        restart: ['config/**', 'constants.ts']
    }
});

With this config, saving anything under src/config/ or src/constants.ts asks for a restart, and so does saving a file that one of them imports. A plugin can add patterns to the same list with registerCriticalFiles.

A save that throws on import

Typos happen. If a file throws while seedcord imports it, its handlers would disappear and the route would stop answering until you fixed it. seedcord keeps the last working version of those handlers registered instead.

Rolled back ./src/handlers/Ping.ts to the last-good version

Ping keeps answering with its old code until your next working save replaces it, so a quick test in Discord can pass against the version you meant to replace. A new file that has never loaded doesn't have a last working version, so it registers nothing until it imports cleanly.

Setting hmr.rollback to false turns rollback off. A broken file drops its handlers, so the route stops answering until you fix it.

seedcord.config.ts
export default defineConfig({
    root: './src',
    instance: './bot.ts',
    build: { start: './index.ts' },
    hmr: {
        rollback: false
    }
});

Type errors while you work

seedcord dev runs your code without checking types, so a type error only reaches you when your editor shows it. A handler with a wrong type still loads and can fail at runtime. Turning on hmr.typecheck runs tsc --watch alongside the bot and reports what it finds on the tsc channel, which you can filter to.

seedcord.config.ts
export default defineConfig({
    root: './src',
    instance: './bot.ts',
    build: { start: './index.ts' },
    hmr: {
        typecheck: true
    }
});

typecheck starts switched off because it costs a second node process. With typecheck: true, tsc waits until your bot is ready, which keeps your first login quick.

typecheck also takes an object with a tsconfig path, for checking against a different tsconfig.

seedcord.config.ts
export default defineConfig({
    root: './src',
    instance: './bot.ts',
    build: { start: './index.ts' },
    hmr: {
        typecheck: { tsconfig: './tsconfig.build.json' }
    }
});

The path resolves against root, like the other paths in hmr.