by Alessio Pelliccione
Some days ago, I was writing a small dynamic script to trigger a build for a single file. While doing that, I suddenly wondered: how do modern build tools actually work under the hood?
That question led me to start building a minimal engine — something simple enough to grasp the core idea, but realistic enough to mirror how tools like esbuild or Vite handle rebuilds internally, deciding what to rebuild.
Let’s see the process step by step — from file watching to dependency graph validation — to see what really happens when you hit “Save”.
What’s actually happening under the hood
When you hit “Save”, your dev server doesn’t rebuild the whole project.
Instead, it follows a smart and very fast chain of steps:
- The file watcher fires an event — tools like
chokidardetect which file changed. - The server looks up that file in its dependency graph — a map of who imports what.
- It invalidates the module and all the modules that depend on it.
- Only those affected modules are rebuilt and reloaded.
That’s it — no full recompilation, no walking through the entire codebase.
This is what makes tools like Vite and esbuild feel instant during development.
Building a minimal smart rebuild engine (with TypeScript code)
Let’s recreate the core idea behind the tools that I mentioned before — but in less than 40 lines of code. The requirements are simple:
- watch files
- detect changes
- compute their hashes
- rebuild only what’s affected
Step 1 — Watch the filesystem
We will use chokidar to detect changes in the src/ directory:
import chokidar from 'chokidar';
const watcher = chokidar.watch('src');
watcher.on('change', (file) => {
console.log(' File changed:', file);
});
Every time you save a file, this watcher emits an event.
Step 2 — Cache file hashes
We don’t want to rebuild if the content didn’t really change (auto‑formatting, whitespace edits). So let’s keep a hash map of file contents:
import crypto from 'crypto';
import fs from 'fs';
const cache = new Map<string, string>();
function hashFile(path: string) {
const content = fs.readFileSync(path, 'utf‑8');
return crypto.createHash('sha1').update(content).digest('hex');
}
Now we can compare the old and the new hash whenever a file changes.
Step 3 — Track dependencies
We’ll store a simple dependency graph to know which files import which:
const graph = new Map<string, string[]>(); // file → [importers]
// Example:
graph.set('src/utils/date.ts', ['src/app.ts']);
graph.set('src/app.ts', ['src/main.ts']);
This tells us that if date.ts changes, app.ts and then main.ts should be invalidated.
Step 4 — Invalidate affected modules
Here’s the rebuild logic — recursive invalidation of dependents:
function invalidate(file: string) {
console.log('♻️ Rebuilding', file);
const dependents = graph.get(file) || [];
dependents.forEach(invalidate);
}
Now we connect everything:
watcher.on('change', (file) => {
const newHash = hashFile(file);
if (cache.get(file) !== newHash) {
cache.set(file, newHash);
invalidate(file);
}
});
And that’s it! You just built the heart of a smart rebuild engine.
The real implementation in my repo is a bit more advanced: it keeps a full dependency graph, incremental cache, and diagnostics for rebuild chains.
You can explore it here: https://github.com/alessiopelliccione/reboost
Understanding HMR (Hot Module Replacement) propagation
Now that our minimal engine knows which modules to rebuild, the next question is: how does the browser update so fast — without a full reload?
The idea
Instead of refreshing the entire page, HMR lets the browser replace only the affected module at runtime. Think of it like a “live patch” system for JavaScript.
When a file changes:
- The dev server detects it (our watcher)
- It invalidates the affected modules
- It sends a WebSocket message to the browser
- The browser dynamically imports the new module
- If the module accepts the update, it’s replaced in place — no full reload
How it connects back to Reboost
In Reboost, this concept becomes even more transparent: each module knows who depends on it through the dependency graph, and when a file changes, the invalidation chain determines how far the rebuild should go before notifying the browser.
Beyond this article
This post only scratches the surface.
My project Reboost is a small experimental engine that goes a step further. It’s not meant to replace modern dev servers, but to understand how they work.
Links
- GitHub: https://github.com/alessiopelliccione/reboost
- Topics:
#vite#esbuild#hmr#javascript#bundler - Related: Vite · Esbuild · HMR · JavaScript bundlers
Written by Alessio Pelliccione
Top comments (0)