Skip to content

Commit 2fe3c7e

Browse files
committed
Initial blogpost on Rewatch
1 parent ffeac09 commit 2fe3c7e

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
---
2+
author: rescript-team
3+
date: "2025-11-11"
4+
badge: roadmap
5+
title: "ReWatch: A Smarter Build System for ReScript"
6+
description: |
7+
ReScript 12 introduces ReWatch, a new build system that brings intelligent dependency tracking, faster incremental builds, and proper monorepo support.
8+
---
9+
10+
## Introduction
11+
12+
ReScript 12 comes with a completely new build system. Internally, we call it ReWatch, though you won't need to invoke it by name (it's now the default when you run `rescript build`). If you've been working with ReScript for a while, you'll know the previous build system (internally called bsb) as a reliable workhorse. But as projects grew larger and monorepos became more common, its limitations became increasingly apparent.
13+
14+
ReWatch addresses these limitations head-on. It brings smarter compilation, significantly faster incremental builds, and proper support for modern development workflows. Let's explore what makes it better.
15+
16+
## Problems We Solved
17+
18+
### Incomplete File Watching in Monorepos
19+
20+
The old build system had trouble tracking file changes across multiple packages in a monorepo. Developers would make changes in one package, but the build system wouldn't always pick them up correctly. This led to stale builds and the dreaded "it works on my machine" moments that could only be fixed with a clean rebuild.
21+
22+
You can see this issue discussed in detail [here](https://github.com/rescript-lang/rescript-lang.org/issues/1090#issuecomment-3361543242).
23+
24+
### Unnecessary Cascade Recompilations
25+
26+
Here's a scenario you might recognize: you add a private helper function to a utility module. The function is completely internal, not exported, and doesn't change any public APIs. Yet the build system recompiles not just that module, but every module that depends on it, and every module that depends on those modules, and so on.
27+
28+
In a large codebase, this could mean recompiling dozens or even hundreds of modules for a change that only affected one file's internal implementation. The wait times added up, breaking the flow of development.
29+
30+
### Slower Builds as Projects Grew
31+
32+
As codebases grew, especially in monorepo setups with multiple packages, the old build system struggled to keep up. The module resolution had to search through many directories, and the dependency tracking wasn't sophisticated enough to avoid unnecessary work.
33+
34+
These weren't just minor inconveniences. They were real productivity drains that got worse as projects scaled.
35+
36+
## The Intelligence Behind ReWatch
37+
38+
ReWatch takes a fundamentally different approach to building your code. At its core is a sophisticated understanding of what actually needs to be rebuilt when files change. Let's break down how it works.
39+
40+
### Understanding CMI Files: The Foundation
41+
42+
Before we dive into ReWatch's innovations, it's worth understanding a key concept: CMI files.
43+
44+
**CMI stands for Compiled Module Interface.** When the compiler processes your ReScript code, it generates several output files:
45+
46+
```
47+
Button.res (your source code)
48+
↓ compiler
49+
├─ Button.mjs # JavaScript code that runs in the browser
50+
├─ Button.cmi # The module's public interface
51+
└─ Button.cmj # Compiled module with implementation details
52+
```
53+
54+
Think of the `.cmi` file as a contract or a table of contents for your module. It describes what other modules can see and import from your module. It contains your type definitions and function signatures, but only the public ones.
55+
56+
Here's a concrete example:
57+
58+
```rescript
59+
// Button.res
60+
type size = Small | Medium | Large
61+
62+
let make = (~size: size, ~onClick) => {
63+
// component implementation
64+
}
65+
66+
let defaultSize = Medium
67+
68+
%%private(let internalHelper = () => {
69+
// some internal logic
70+
})
71+
```
72+
73+
The `.cmi` file for this module will contain:
74+
- The `size` type definition
75+
- The signature of `make`
76+
- The type of `defaultSize`
77+
78+
But it won't contain `internalHelper` because it's marked as [`%%private`](https://rescript-lang.org/syntax-lookup#private-let), making it truly internal to the module.
79+
80+
**This distinction is crucial for build performance.** If you change `internalHelper`, the `.cmi` file stays exactly the same because the public interface didn't change. But if you add a parameter to `make` or change the `size` type, the `.cmi` file changes because the public contract changed.
81+
82+
### Smart Dependency Tracking Through Interface Stability
83+
84+
ReWatch uses CMI files to make intelligent decisions about what needs recompiling. Here's how:
85+
86+
When you change a file, ReWatch:
87+
1. Computes a hash of the current `.cmi` file (before compilation)
88+
2. Compiles the changed module
89+
3. Computes a hash of the new `.cmi` file (after compilation)
90+
4. Compares the two hashes
91+
92+
If the hashes match, the module's public interface hasn't changed. ReWatch then knows it can skip recompiling dependent modules.
93+
94+
Let's see this in action. Imagine you refactor the internal implementation of a `getClassName` helper in your Button component:
95+
96+
```rescript
97+
// Button.res - BEFORE
98+
let make = (~size, ~onClick) => {
99+
let className = getClassName(size)
100+
// render button
101+
}
102+
103+
let getClassName = (size) => {
104+
switch size {
105+
| Small => "btn-sm"
106+
| Medium => "btn-md"
107+
| Large => "btn-lg"
108+
}
109+
}
110+
```
111+
112+
You decide to refactor `getClassName` to use a map:
113+
114+
```rescript
115+
// Button.res - AFTER
116+
let make = (~size, ~onClick) => {
117+
let className = getClassName(size)
118+
// render button
119+
}
120+
121+
let getClassName = (size) => {
122+
// New implementation using a map
123+
sizeMap->Map.get(size)->Option.getOr("btn-md")
124+
}
125+
```
126+
127+
The internal implementation changed, but the public API (the `make` function signature) stayed the same. The `.cmi` file is identical before and after.
128+
129+
**With the old build system:**
130+
- Button.res changes → recompile Button
131+
- Check all dependents of Button → recompile them too
132+
- Check all their dependents → recompile those as well
133+
- Result: potentially dozens of modules recompiled
134+
135+
**With ReWatch:**
136+
- Button.res changes → recompile Button
137+
- Check Button.cmi hash → unchanged
138+
- Skip recompiling dependents
139+
- Result: one module recompiled
140+
141+
This is the key innovation. ReWatch doesn't just track which modules depend on each other. It understands when those dependencies actually matter.
142+
143+
### When Recompilation is Necessary
144+
145+
Of course, when you do change a public interface, ReWatch correctly identifies all affected modules:
146+
147+
```rescript
148+
// Button.res - Adding a new parameter
149+
let make = (~size, ~onClick, ~disabled=false) => {
150+
// ...
151+
}
152+
```
153+
154+
Now the `.cmi` file changes because the function signature changed. ReWatch detects this and recompiles all modules that use `Button.make`. This is the correct behaviour, but it only happens when truly necessary.
155+
156+
### Explicit Interface Files for Maximum Control
157+
158+
If you want even more control over your module's public interface, you can use explicit `.resi` files:
159+
160+
```rescript
161+
// Button.resi
162+
type size = Small | Medium | Large
163+
let make: (~size: size, ~onClick: unit => unit) => Jsx.element
164+
let defaultSize: size
165+
```
166+
167+
With an explicit interface file, the `.cmi` is generated from the `.resi` file. Any changes to `Button.res` that don't affect the `.resi` contract will never trigger dependent recompilations. This gives you maximum build performance for frequently-changed modules.
168+
169+
**Bonus for React developers:** Using `.resi` files for your React components has another benefit. During development, when you modify the component's internal implementation without changing the interface, React's [Fast Refresh](https://www.gatsbyjs.com/docs/reference/local-development/fast-refresh/) can preserve component state more reliably. Combined with ReWatch's intelligent rebuilding, this makes for an exceptionally smooth development experience.
170+
171+
### Faster Module Resolution with Flat Directory Layout
172+
173+
ReWatch employs another clever optimization for module resolution. When building your project, it copies all source files to a flat directory structure at `lib/ocaml/`. Instead of maintaining the original nested directory structure, every module ends up in one place.
174+
175+
Think of it like organizing a library. The old approach was like having books scattered across multiple rooms and floors. To find a specific book, you'd need to check each room. ReWatch's approach is like putting all the books in one room. Finding what you need is instant.
176+
177+
**Why this matters:**
178+
- Module lookup becomes a single directory operation
179+
- The filesystem cache is more effective when files are adjacent
180+
- Cross-package references are as fast as local references
181+
- The compiler spends less time searching and more time compiling
182+
183+
The small cost of copying files upfront is paid back many times over through faster compilation.
184+
185+
### Wave-Based Parallel Compilation
186+
187+
ReWatch compiles your modules in dependency-order waves, with parallel processing within each wave.
188+
189+
Here's how it works: modules with no pending dependencies compile first, in parallel. As they complete, the next wave of modules (whose dependencies are now satisfied) begins compiling. This continues until all modules are built.
190+
191+
Combined with the CMI hash checking, this means:
192+
- Maximum parallelism within each wave
193+
- Waves can terminate early if interface stability is detected
194+
- No wasted work on modules that don't need rebuilding
195+
196+
### Proper Monorepo Support
197+
198+
ReWatch was designed from the ground up with monorepos in mind. File watching works correctly across all packages, detecting changes wherever they occur. The formatter is also properly integrated, working seamlessly across the entire monorepo structure.
199+
200+
These aren't just nice-to-haves. For teams working with multiple packages, proper monorepo support means the build system finally works the way you'd expect.
201+
202+
## What This Means for Your Workflow
203+
204+
The techniques described above work together to create a build system that feels fundamentally different in practice. The most common operations during development (refactoring internal logic, fixing bugs, tweaking implementations) typically result in sub-second rebuilds. Less common operations that truly do require cascade recompilations (API changes, type modifications) benefit from parallel compilation and better module resolution.
205+
206+
There's a threshold in human perception where interactions that complete in under 2 seconds feel instant. ReWatch aims to keep most of your development builds under this threshold, keeping you in flow rather than waiting for compilation.
207+
208+
## Package Manager Compatibility
209+
210+
ReWatch works with the major package managers: npm, yarn, pnpm, deno, and bun.
211+
212+
**Note on Bun:** Recent versions of Bun (1.3+) default to "isolated" mode for monorepo installations, which can cause issues with ReWatch. If you're using Bun, you'll need to configure it to use hoisted mode by adding this to your `bunfig.toml`:
213+
214+
```toml
215+
[install]
216+
linker = "hoisted"
217+
```
218+
219+
We're continuing to test compatibility across different environments and configurations. If you encounter issues with any package manager, please report them so we can address them.
220+
221+
## Using the Legacy Build System
222+
223+
For projects that need it, the legacy bsb build system remains available through the `rescript-legacy` command. This is a separate binary, not a subcommand.
224+
225+
```bash
226+
# Build your project
227+
rescript-legacy build
228+
229+
# Build with watch mode
230+
rescript-legacy build -w
231+
232+
# Clean build artifacts
233+
rescript-legacy clean
234+
```
235+
236+
You can add these to your `package.json` scripts:
237+
238+
```json
239+
{
240+
"scripts": {
241+
"build": "rescript-legacy build",
242+
"watch": "rescript-legacy build -w",
243+
"clean": "rescript-legacy clean"
244+
}
245+
}
246+
```
247+
248+
The legacy system might be needed temporarily for compatibility with specific tooling or during migration. However, we strongly encourage moving to ReWatch to take advantage of the performance improvements and better monorepo support.
249+
250+
The default `rescript` command now uses ReWatch. If you've been using `rescript build` or `rescript -w`, they'll automatically use the new system.
251+
252+
## Conclusion
253+
254+
ReWatch represents a significant leap forward for ReScript's build tooling. The intelligent dependency tracking means less waiting and more building. The proper monorepo support means it scales with your project structure. The performance improvements are most noticeable where they matter most: during iterative development.
255+
256+
These improvements come from fundamental changes in how the build system understands and processes your code. By tracking interface stability rather than just file changes, ReWatch does less work while being smarter about what actually needs rebuilding.
257+
258+
If you're upgrading to ReScript 12, you'll get ReWatch automatically. We're excited to hear how it works for your projects. As always, feedback and bug reports are welcome. You can reach us through the forum or on GitHub.
259+
260+
Happy building!

0 commit comments

Comments
 (0)