|
| 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