Skip to content

Commit 20227bb

Browse files
committed
Merge branch 'master' of github.com:rescript-lang/rescript-lang.org into vlk-v12-react-router
2 parents 666f235 + 8baea45 commit 20227bb

File tree

3 files changed

+631
-111
lines changed

3 files changed

+631
-111
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
---
2+
author: rescript-team
3+
date: "2025-11-04"
4+
previewImg: /static/blog/rescript-12-reforging-build-system.png
5+
articleImg: /static/blog/rescript-12-reforging-build-system.png
6+
badge: roadmap
7+
title: "Reforging the ReScript Build System"
8+
description: |
9+
ReScript 12 introduces a completely new build system that brings intelligent dependency tracking, faster incremental builds, and proper monorepo support.
10+
---
11+
12+
## Introduction
13+
14+
ReScript 12 comes with a completely new build system. Internally, we call it Rewatch, though you will not need to invoke it by name (it is now the default when you run `rescript build`). If you have been working with ReScript for a while, you will know the previous build system (internally called bsb) as a reliable workhorse. As projects grew larger and monorepos became more common, however, its limitations became increasingly apparent.
15+
16+
The new system addresses these limitations directly. It brings smarter compilation, significantly faster incremental builds, and proper support for modern development workflows.
17+
18+
## The Evolution from Single Packages to Monorepos
19+
20+
The previous build system worked well for single-package projects, providing efficient incremental builds and avoiding unnecessary recompilations when module interfaces didn't change. For many projects, it was perfectly adequate.
21+
22+
However, as the ReScript ecosystem matured and teams began adopting monorepo structures with multiple interdependent packages, specific limitations became apparent.
23+
24+
### Watch Mode in Monorepos
25+
26+
The most significant pain point was watch mode. While bsb could build monorepos, its watch mode only tracked files within the current package. If you had Package A depending on Package B, changes to Package B would not trigger rebuilds in Package A's watch mode. Developers had to manually rebuild or run separate watchers for each package.
27+
28+
You can see this issue discussed in detail [here](https://github.com/rescript-lang/rescript-lang.org/issues/1090#issuecomment-3361543242).
29+
30+
### Sequential Dependency Builds
31+
32+
When building multiple packages, bsb processed them sequentially rather than in parallel. In a monorepo with five packages, they would build one after another, even when some could build simultaneously. This meant unused parallelization opportunities.
33+
34+
### Per-Package Build Isolation
35+
36+
Each package ran in its own Ninja process with no shared understanding across the monorepo. This meant no cross-package optimization opportunities and multiplied process startup overhead.
37+
38+
## Why a Complete Rewrite?
39+
40+
To understand why the new build system represents such a significant improvement, it helps to understand what the old build system actually was.
41+
42+
### The Legacy Architecture: Ninja-Based
43+
44+
The previous build system was a wrapper around [Ninja](https://ninja-build.org/), a generic build system originally created by Google for building Chrome. It would scan your ReScript source files, generate a `build.ninja` file describing all compilation rules, and Ninja would execute the builds in parallel.
45+
46+
This architecture served ReScript well for years, providing solid build performance for single-package projects.
47+
48+
### The Limitations of a Generic Build System
49+
50+
Ninja was designed for C++ compilation and had no native understanding of concepts crucial for modern ReScript workflows:
51+
52+
- Monorepo package boundaries and inter-package dependencies
53+
- Coordinated watching across multiple packages
54+
- Parallel builds across package boundaries
55+
- Cross-package optimization opportunities
56+
57+
The wrapper approach meant every ReScript-specific feature had to be translated into Ninja's generic model. This translation layer worked well for single packages but became limiting as monorepo adoption grew.
58+
59+
### A Purpose-Built Solution
60+
61+
Rewatch started at [Walnut](https://walnut.io), where [Jaap Frolich](https://github.com/jfrolich) and [Roland Peelen](https://github.com/rolandpeelen) built it to solve real problems they were facing with large ReScript codebases. It is now part of the official ReScript compiler distribution.
62+
63+
Written specifically for ReScript in Rust, the new build system has native understanding of ReScript's compilation model. There is no translation layer. It directly understands:
64+
65+
- ReScript's compilation phases (parsing, type checking, code generation)
66+
- The meaning and role of CMI, CMT, and CMJ files
67+
- Module dependency graphs spanning multiple packages
68+
- Package boundaries and monorepo structures
69+
- When and how to coordinate builds across packages
70+
71+
This deep integration enables features that were difficult or impossible with a wrapper around a generic build system:
72+
73+
- **Unified watch mode** that tracks changes across all packages in a monorepo
74+
- **Parallel package builds** instead of sequential processing
75+
- **Explicit hash-based interface checking** that's more reliable than timestamp-based mechanisms
76+
- **Integrated formatter** that works seamlessly across package boundaries
77+
- **Better error messages** with full context about where in the build pipeline issues occurred
78+
79+
The rewrite also opens doors for future improvements that require tight integration: incremental type checking, distributed build caching, and better language server integration.
80+
81+
## The Intelligence Behind the New Build System
82+
83+
The new build system 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.
84+
85+
### How ReScript Compilation Works
86+
87+
ReScript's compilation model differs from many other languages in important ways.
88+
89+
**ReScript compiles one file at a time, in complete isolation.** When you compile `Button.res`, the compiler does not maintain any shared state with other modules. Each file produces its own self-contained set of artifacts: the JavaScript output (`.mjs` or whatever extension you specified in your `rescript.json`), the module interface (`.cmi`), the typed tree (`.cmt`), and optimization metadata (`.cmj`). There is no separate linking phase like in C/C++, and no whole-program analysis like in many bundlers.
90+
91+
Compilation happens in two phases: first, the file is parsed into an abstract syntax tree, then that tree is type-checked and compiled to JavaScript. This two-phase approach gives the build system fine-grained control over what work to skip. If a file has not changed at all, both phases can be skipped. If a dependency's public interface did not change (even though the file was modified), dependent modules can skip recompilation entirely.
92+
93+
Additionally, ReScript's module system enforces a strict constraint: **dependency cycles are not allowed.** Module A cannot depend on Module B while Module B also depends on Module A. This means the module graph is always a DAG ([Directed Acyclic Graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph)).
94+
95+
Most languages compile like cooking a complex dish where all ingredients affect each other. ReScript compiles like an assembly line where each station produces a complete, independent part. This makes it straightforward to parallelize, cache, and optimize.
96+
97+
**Why this matters for build performance:**
98+
99+
- **Perfect caching:** Since files compile independently with no global state, cached artifacts are always safe to reuse
100+
- **Trivial parallelization:** No coordination needed between parallel compilations since they do not share state
101+
- **Precise incremental rebuilds:** Changes propagate predictably through the DAG with clear stopping points
102+
- **Foundation for future optimizations:** This model enables possibilities like distributed compilation and build caching across CI runs
103+
104+
**The trade-off:** This approach limits some whole-program optimizations, but the gains in predictability and speed are substantial. More importantly, this clean, constrained model is exactly what makes the sophisticated optimizations possible. The CMI hash checking, wave-based compilation, and early termination strategies all build on these fundamental properties.
105+
106+
### Understanding CMI Files
107+
108+
A specific artifact is central to the build system's optimizations: the CMI file.
109+
110+
**CMI stands for Compiled Module Interface.** When the compiler processes your ReScript code, it always generates several output files:
111+
112+
```
113+
Button.res (your source code)
114+
↓ compiler
115+
├─ Button.mjs # JavaScript output
116+
├─ Button.cmi # Module's public API signature
117+
├─ Button.cmt # Typed AST
118+
└─ Button.cmj # Optimization metadata (function arity, cross-module inlining)
119+
```
120+
121+
The `.cmi` file is automatically generated for every module. It acts as a contract or table of contents that describes what other modules can see and import from your module. It contains your type definitions and function signatures, but only the public ones.
122+
123+
If you do not write an explicit interface file (`.resi`), the compiler infers the interface from everything you export in the `.res` file. If you do write a `.resi` file, that becomes the explicit interface instead.
124+
125+
Here's a concrete example with an explicit [interface file](/docs/manual/v12.0.0/module#signatures):
126+
127+
```rescript
128+
// Button.resi
129+
type size = Small | Medium | Large
130+
let make: (~size: size, ~onClick: unit => unit) => Jsx.element
131+
let defaultSize: size
132+
```
133+
134+
```rescript
135+
// Button.res
136+
type size = Small | Medium | Large
137+
138+
let make = (~size: size, ~onClick) => {
139+
// component implementation
140+
}
141+
142+
let defaultSize = Medium
143+
144+
// Not in interface file - this is private
145+
let internalHelper = () => {
146+
// some internal logic
147+
}
148+
```
149+
150+
The `.cmi` file for this module will contain:
151+
152+
- The `size` type definition
153+
- The signature of `make`
154+
- The type of `defaultSize`
155+
156+
But it will not contain `internalHelper` because it is not listed in the `.resi` file, making it truly internal to the module.
157+
158+
**This distinction is crucial for build performance.** If you change `internalHelper`, the `.cmi` file stays exactly the same because the public interface did not change. But if you add a parameter to `make` or change the `size` type, the `.cmi` file changes because the public contract changed.
159+
160+
**Tip for React developers:** Using `.resi` files for your components has an additional benefit. When you modify a 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 during development, creating an exceptionally smooth development experience.
161+
162+
### Hash-Based Interface Stability Detection
163+
164+
To detect whether a module's interface has changed, the build system computes a hash of the `.cmi` file before and after compilation. If the hashes match, dependent modules can skip recompilation.
165+
166+
The previous system used timestamp-based detection through Ninja's `restat` feature, which worked well for single packages. While both approaches aim to avoid unnecessary recompilation, the explicit hash-based method provides more predictable behaviour, especially when dealing with timestamp issues across filesystems or in containerized environments. It also provides consistent behaviour across package boundaries in monorepos.
167+
168+
### Faster Module Resolution with Flat Directory Layout
169+
170+
The build system employs another optimization for module resolution. When building your project, it copies all source files to a flat directory structure in the build output. Instead of maintaining the original nested directory structure, every module ends up in one place.
171+
172+
The old approach scattered modules across multiple directories, like books spread across multiple rooms and floors. Finding a specific module required checking each location. The new approach places all modules in one location, making lookups instant.
173+
174+
**Why this matters:**
175+
176+
- Module lookup becomes a single directory operation
177+
- The filesystem cache is more effective when files are adjacent
178+
- Cross-package references are as fast as local references
179+
- The compiler spends less time searching and more time compiling
180+
181+
The small cost of copying files upfront is paid back many times over through faster compilation.
182+
183+
Note that filesystems can struggle when there are too many files in a single directory. Since the build system controls the output layout, it can transparently shard files into subdirectories as a future optimization if needed, without any changes required from users.
184+
185+
### Wave-Based Parallel Compilation
186+
187+
Compilation is organized into waves based on dependency order, similar to an assembly line where some stations can run in parallel while others must wait for earlier stations to complete.
188+
189+
Consider this dependency structure:
190+
191+
```
192+
A
193+
╱ ╲
194+
B C
195+
│ │
196+
D E
197+
╲ ╱
198+
F
199+
```
200+
201+
This is processed in waves:
202+
203+
**Wave 1:** Compile A (no dependencies)
204+
**Wave 2:** Compile B and C in parallel (both depend only on A, which is done)
205+
**Wave 3:** Compile D and E in parallel (their dependencies are satisfied)
206+
**Wave 4:** Compile F (waits for both D and E)
207+
208+
The key insight: within each wave, all modules compile simultaneously. The build system identifies which modules are ready (all their dependencies are compiled) and processes them together.
209+
210+
Combined with CMI hash checking, this becomes even more powerful. If module A's interface does not change, modules B and C might skip actual compilation even though they are queued in Wave 2. They pass through the wave without doing unnecessary work.
211+
212+
This approach emerged naturally from solving the problem of maximizing parallel compilation while respecting dependencies. The solution corresponds to a classic computer science algorithm: [Kahn's algorithm](https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm) for topological sorting.
213+
214+
### Proper Monorepo Support
215+
216+
The build system was designed from the ground up with monorepos in mind. It automatically detects the parent-child relationship between your monorepo root and its packages by examining `rescript.json` files and package dependencies.
217+
218+
This detection means commands work intuitively wherever you run them:
219+
220+
- **Building from the root** builds all local packages
221+
- **Building from a child package** builds just that package, with full knowledge of the parent for resolving dependencies
222+
- **Clean and format commands** follow the same pattern: operate on all packages from the root, or on a single package from a child
223+
224+
File watching works correctly across all packages, detecting changes wherever they occur. This works with npm workspaces, yarn workspaces, pnpm, and other package managers that use symlinking for local dependencies.
225+
226+
## A Unified Developer Experience
227+
228+
Beyond the build performance improvements, ReScript 12 brings a completely redesigned command-line interface. The new system consolidates everything into one cohesive tool:
229+
230+
```bash
231+
rescript # Defaults to build
232+
rescript build # Explicit build
233+
rescript watch # Build + watch mode
234+
rescript clean # Clean artifacts
235+
rescript format # Format code
236+
```
237+
238+
**Smart defaults:** Running `rescript` without arguments builds your project.
239+
240+
**Consistent interface:** All commands follow the same patterns and use the same help system.
241+
242+
**Better error messages:** Clear, contextual errors instead of multi-layer stack traces.
243+
244+
**Reliable process handling:** Ctrl+C in watch mode always cleans up properly.
245+
246+
**Integrated formatting:** Format your entire project, specific files, or use check mode for CI, all with parallel processing.
247+
248+
## Package Manager Compatibility
249+
250+
The build system works with the major package managers: npm, yarn, pnpm, deno, and bun.
251+
252+
**Note on Bun:** Recent versions of Bun (1.3+) default to "isolated" mode for monorepo installations, which can cause issues. If you are using Bun, you will need to configure it to use hoisted mode by adding this to your `bunfig.toml`:
253+
254+
```toml
255+
[install]
256+
linker = "hoisted"
257+
```
258+
259+
We are 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.
260+
261+
## Using the Legacy Build System
262+
263+
For projects that need it, the legacy build system remains available throughout the v12 release cycle through the `rescript-legacy` command. This is a separate binary, not a subcommand. We expect to remove it in v13, so we encourage migrating to the new system when possible.
264+
265+
```bash
266+
# Build your project
267+
rescript-legacy build
268+
269+
# Build with watch mode
270+
rescript-legacy build -w
271+
272+
# Clean build artifacts
273+
rescript-legacy clean
274+
```
275+
276+
You can add these to your `package.json` scripts:
277+
278+
```json
279+
{
280+
"scripts": {
281+
"build": "rescript-legacy build",
282+
"watch": "rescript-legacy build -w",
283+
"clean": "rescript-legacy clean"
284+
}
285+
}
286+
```
287+
288+
The legacy system might be needed temporarily for compatibility with specific tooling or during migration. However, we strongly encourage moving to the new build system to take advantage of the performance improvements and better monorepo support.
289+
290+
The default `rescript` command now uses the new build system. If you have been using `rescript build` or `rescript -w`, they will automatically use it.
291+
292+
## Conclusion
293+
294+
ReScript 12's new build system brings together intelligent dependency tracking, proper monorepo support, and a unified developer experience. The improvements are most noticeable in monorepo setups, but all projects benefit from faster module resolution, integrated formatting, and more reliable build orchestration.
295+
296+
## Acknowledgments
297+
298+
Our deep appreciation goes to Jaap Frolich and Roland Peelen for creating Rewatch. What started as solving their own build challenges at Walnut has become a fundamental improvement for the entire ReScript community. The research and engineering effort they invested in understanding compiler internals and reimagining dependency tracking has made a real difference for every ReScript developer. Thank you to Walnut for supporting this work and sharing it with the community.
299+
300+
If you are upgrading to ReScript 12, you will get the new build system automatically. We are excited to hear how it works for your projects. As always, feedback and bug reports are welcome. You can reach us through the [forum](https://forum.rescript-lang.org/) or on [GitHub](https://github.com/rescript-lang/rescript).
301+
302+
Happy building!

0 commit comments

Comments
 (0)