# ReScript Build System Focused documentation for ReScript build configuration, project structure, monorepo setup, compiler performance, and build-tool integration. # Build System Overview ReScript comes with a build system, [`rescript`](https://www.npmjs.com/package/rescript), that's fast, lean and used as the authoritative build system of the community. Every ReScript project needs a build description file, `rescript.json`. ## Options See `rescript help`: ``` ❯ rescript help Usage: rescript `rescript` is equivalent to `rescript build` Options: -v, -version display version number -h, -help display help Subcommands: build clean format convert dump help Run `rescript -h` for subcommand help. Examples: rescript build -h rescript format -h ``` ## Build Project Each build will create build artifacts from your project's source files. **To build a project (including its dependencies)**, run: ```sh rescript ``` Which is an alias for `rescript build`. To keep a build watcher, run: ```sh rescript -w ``` Any new file change will be picked up and the build will re-run. **Note**: third-party libraries in `node_modules` aren't watched, as doing so may exceed the node.js watcher count limit. For monorepo setup in ReScript 12+, see [Setting up a monorepo](./build-monorepo-setup.mdx). ## Clean Project If you ever get into a stale build for edge-case reasons, use: ```sh rescript clean ``` ## Compile with stricter errors in CI **Since 11.1** You may want to compile your project with stricter rules for production, than when developing. With the `-warn-error` build flag, this can easily be done, for instance in a continuous integration script. E.g.: ```sh rescript -warn-error +110 ``` Here, warning number 110, which is triggered when a [`%todo`](../../syntax-lookup/extension_todo.mdx) has been found, gets promoted to an error. The full list of warning numbers can be found [here](./warning-numbers.mdx). # Configuration `rescript.json` is the single, mandatory build meta file needed for `rescript`. **The complete configuration schema is [here](./build-configuration-schema.mdx)**. We'll _non-exhaustively_ highlight the important parts in prose below. ## name, namespace `name` is the name of the library, used as its "namespace". You can activate namespacing through `"namespace": true` in your `rescript.json`. Namespacing is almost **mandatory**; we haven't turned it on by default yet to preserve backward-compatibility. **Explanation**: by default, your files, once used as a third-party dependency, are available globally to the consumer. E.g. if you have a `Util.res` and the consumer also has a file of the same name, they will clash. Turning on `namespace` avoids this by wrapping all your own project's files into an extra module layer; instead of a global `Util` module, the consumer will see you as `MyProject.Util`. **The namespacing affects your consumers, not yourself**. Aka, in ReScript, "namespace" is just a fancy term for an auto-generated module that wraps all your project's files (efficiently and correctly, of course!) for third-party consumption. We don't do folder-level namespacing for your own project; all your own file names must be unique. This is a constraint that enables several features such as fast search and easier project reorganization. **Note**: the `rescript.json` `name` should be the same as the `package.json` `name`, to avoid confusing corner-cases. However, this means that you can't use a camelCased names such as `MyProject`, since `package.json` and npm forbid you to do so (some file systems are case-insensitive). To have the namespace/module as `MyProject`, write `"name": "my-project"`. ReScript will turn that into the camelCased name correctly. **Note on custom namespacing**: if for some reason, you need a namespace that is different from what your `name` will produce, you can directly send a string to the `namespace` option. For example, if your package is a binding named `bs-some-thing`, you can use `"namespace": "some-thing"` to get `SomeThing` namespace instead of `BsSomeThing`. ## sources Your source files need to be specified explicitly (we don't want to accidentally drill down into some unrelated directories). Examples: ```json { "sources": ["src", "examples"] } ``` ```json { "sources": { "dir": "src", "subdirs": ["page"] } } ``` ```json { "sources": [ "examples", { "dir": "src", "subdirs": true // recursively builds every subdirectory } ] } ``` You can mark your directories as dev-only (for e.g. tests). These won't be built and exposed to third-parties, or even to other "dev" directories in the same project: ```json { "sources": { "dir": "test", "type": "dev" } } ``` You can also explicitly allow which modules can be seen from outside. This feature is especially useful for library authors who want to have a single entry point for their users. Here, the file `src/MyMainModule.res` is exposed to outside consumers, while all other files are private. ```json { "sources": { "dir": "src", "public": ["MyMainModule"] } } ``` ## dependencies, dev-dependencies List of ReScript dependencies. Just like `package.json`'s dependencies, they'll be searched in `node_modules`. Note that only sources marked with `"type":"dev"` will be able to resolve modules from `dev-dependencies`. > The legacy keys `bs-dependencies` and `bs-dev-dependencies` are still accepted but deprecated. ## js-post-build Hook that's invoked every time a file is recompiled. Good for JS build system interop, but please use it **sparingly**. Calling your custom command for every recompiled file slows down your build and worsens the building experience for even third-party users of your lib. Example: ```json { "js-post-build": { "cmd": "/path/to/node ../../postProcessTheFile.js" } } ``` Note that the path resolution for the command (`node` in this case) is done so: - `/myCommand` is resolved into `/myCommand` - `package/myCommand` is resolved into `node_modules/package/myCommand` - `./myCommand` is resolved into `myProjectRoot/myCommand` - `myCommand` is just called as `myCommand`, aka a globally available executable. But note that ReScript doesn't read into your shell's environment, so if you put e.g. `node`, it won't find it unless you specify an absolute path. Alternatively, add `#!/usr/local/bin/node` to the top of your script to directly call it without prepending `node`. The command itself is called from inside `lib/bs`. ## jsx Controls how the compiler emits JSX and which runtime (if any) it delegates to. A minimal configuration looks like this: ```json { "jsx": { "version": 4 } } ``` - `version`: JSX transform version. `4` enables the React 17+ transform and is the current and only supported option in v12. - `module`: Override the target module that receives JSX calls. Useful for [generic JSX transforms](./jsx.mdx#generic-jsx-transform-jsx-beyond-react-experimental); omit it when using the built-in React runtime. - `preserve`: When `true`, the compiler re-emits JSX syntax in the generated JavaScript so bundlers or other tooling can take over the transform later. The regenerated JSX might differ slightly from the original source but stays semantically equivalent. See [Preserve mode](./jsx.mdx#preserve-mode) for details. All fields are optional; unspecified fields fall back to the defaults mentioned above. Combine them as needed for your project's JSX runtime. ## package-specs Output to either CommonJS (the default) or JavaScript module. Example: ```json { "package-specs": { "module": "commonjs", "in-source": true } } ``` - `"module": "commonjs"` generates output as CommonJS format. - `"module": "esmodule"` generates output as JavaScript module format. Will be default value in next major. - `"in-source": true` generates output alongside source files. If you omit it, it'll generate the artifacts into `lib/js`. The output directory is not configurable otherwise. This configuration only applies to you, when you develop the project. When the project is used as a third-party library, the consumer's own `rescript.json` `package-specs` overrides the configuration here, logically. ## suffix **Since 11.0**: The suffix can be freely chosen. However, we still suggest you stick to the convention and use one of the following: - `".js` - `".mjs"` - `".cjs"` - `".res.js"` - `".res.mjs"` - `".res.cjs"` ### Design Decisions Generating JS files with the `.res.js` suffix means that, on the JS side, you can do `const myReScriptFile = require('./TheFile.res.js')`. The benefits: - It's immediately clear that we're dealing with a generated JS file here. - It avoids clashes with a potential `TheFile.js` file in the same folder. - It avoids the need of using a build system loader for ReScript files. This + in-source build means integrating a ReScript project into your pure JS codebase **basically doesn't touch anything in your build pipeline at all**. ## warnings Selectively turn on/off certain warnings and/or turn them into hard errors. Example: ```json { "warnings": { "number": "-44-102", "error": "+5" } } ``` Turn off warning `44` and `102` (polymorphic comparison). Turn warning `5` (partial application whose result has function type and is ignored) into a hard error. The warning numbers are shown in the build output when they're triggered. See [Warning Numbers](./warning-numbers.mdx) for the complete list. ## compiler-flags Extra flags to pass to the compiler. For advanced usages. - `-open ABC` opens the module `ABC` for each file in the project. `ABC` can either be a dependency, namespaced project or local module of the current project. > The legacy key `bsc-flags` is still accepted but deprecated. ## gentypeconfig To enable genType, set `"gentypeconfig"` at top level in the project's `rescript.json`. ```json { "gentypeconfig": { "module": "esmodule", "moduleResolution": "node", "generatedFileExtension": ".gen.tsx", "debug": { "all": false, "basic": false } } } ``` `generatedFileExtension`: File extension used for genType generated files (defaults to `".gen.tsx"`) `module`: Module format used for the generated `*.gen.tsx` files (supports `"esmodule"` and `"commonjs"`) `moduleResolution`: Module resolution strategy used in genType outputs. This may be required for compatibility with TypeScript projects. Specify the value as the same in `tsconfig.json`. - `"node"`(default): Drop extensions in import paths. - `"node16"`: Use TS output's extension. This provides compatibility with projects using `"moduleResolution": "node16"` and ES Modules. - `"bundler"`: Use TS input's extension. This provides compatibility with projects using `"moduleResolution": "bundler"` and ES Modules. This also requires TS v5.0+ and `compilerOptions.allowImportingTsExtensions` to `true` `debug`: Enable debug logs. ## Environment Variables We heavily disrecommend the usage of environment variables, but for certain cases, they're justified. ### Error Output Coloring: `FORCE_COLOR` This is mostly for other programmatic usage of `rescript` where outputting colors is not desired. When `FORCE_COLOR` is set to `1`: `rescript` produces color. When `FORCE_COLOR` is set to `0`: `rescript` doesn't produce color. When `FORCE_COLOR` is not set: `rescript` might or might not produce color, depending on a smart detection of where it's outputted. > Note that the underlying compiler will always be passed `-color always`. See more details in [this issue](https://github.com/rescript-lang/rescript-compiler/issues/2984#issuecomment-410669163). # Setting up a monorepo with ReScript **Since 12.0** > A monorepo is a single repository containing multiple separate projects, with clear relationships between them. ReScript 12.0 introduces improved support for native monorepos through the new ["Rewatch"](../../blog/reforging-build-system.mdx) build system. This guide walks you through the setup process. **Note:** This feature requires the new build system and is **not compatible** with `rescript-legacy`. ## Project Structure A ReScript monorepo requires a `rescript.json` file at the repository root, plus a `rescript.json` file in each sub-project directory. Basically, the monorepo contains a root package that manages all local dependencies. Building the root package will build all its dependencies. **Important:** You also need a node_modules monorepo setup with symlinks. In practice, if you want a ReScript monorepo, you will also need an npm/yarn/pnpm/bun monorepo. A typical structure looks like this: ``` my-monorepo/ ├── rescript.json ├── package.json ├── node_modules/ │ ├── package-1/ # symlinked │ ├── package-2/ # symlinked ├── packages/ │ ├── package-1/ │ │ ├── rescript.json │ │ ├── package.json │ │ ├── src/ │ ├── package-2/ │ │ ├── rescript.json │ │ ├── package.json │ │ ├── src/ │ ├── ... ``` ## Root `rescript.json` Configuration The root `rescript.json` manages the monorepo by listing its packages. ```json { "name": "my-monorepo", "dependencies": ["package-1", "package-2"], "package-specs": { "module": "esmodule", "in-source": true }, "suffix": ".res.mjs", "compiler-flags": [] } ``` The `"dependencies"` array lists the names of your packages, which must match the `"name"` fields in their respective sub-rescript.json files. When you build a package in ReScript, it will use the `"package-specs"` and `"suffix"` settings from the root package. Therefore, it is recommended to place these settings in the root `rescript.json` file and avoid specifying them in local package `rescript.json` files. **Settings from different config files:** When Rewatch builds a package within a monorepo setup, it uses these settings from the root rescript.json: - `"jsx"` (jsx_args, jsx_module_args, jsx_mode_args, jsx_preserve_args) - `"experimental"` (experimental_features_args) - `"package-specs"` (used for implementation_args) - `"suffix"` (used for package output) These settings come from the package's own rescript.json: - `"sources"` (determines which files to compile) - `"dependencies"` (package dependencies) - `"warnings"` (warning_args) - `"compiler-flags"` (bsc_flags) When the root package is built, Rewatch will look for the dependencies inside the `my-monorepo/node_modules` folder. It is expected that `package-1` and `package-2` are available there via a symlink system provided by your node_modules package manager. Note that your root rescript.json is allowed to have a `"sources"` setting. These files will be compiled as expected. ## Package `rescript.json` Configuration Each nested rescript.json sets up a specific package. `packages/package-1/rescript.json`: ```json { "name": "package-1", "sources": ["src"], "dependencies": [], "compiler-flags": ["-open Foobar"] } ``` `packages/package-2/rescript.json`: ```json { "name": "package-2", "sources": ["src"], "dependencies": ["package-1"], "warnings": { "number": "-27" } } ``` In `package-1`, we show how to use special compiler flags. In `package-2`, we show how to disable warning 27 (unused variable). In both cases, the settings only apply to the package where they are specified. Defining these in the root rescript.json will not affect the packages. There is no inheritance system. Also note the dependencies array in `package-2`, which allows that package to depend on `package-1` within the monorepo. ## Building the monorepo From the root directory, you can run all ReScript commands: ```bash # Build all packages rescript build # Clean all packages rescript clean # Format all packages rescript format ``` ### Building individual packages You can also run ReScript commands on individual packages instead of the entire monorepo. This is useful when you only want to work on one package. ```bash # Build from the package directory cd packages/package-3 rescript build rescript clean rescript format # Or run from the root directory rescript build packages/package-3 rescript clean packages/package-3 rescript format packages/package-3 ``` When building a single package, ReScript will use the settings from the root rescript.json as explained in the [Root rescript.json Configuration](#root-rescriptjson-configuration) section above. ### Building without a root rescript.json If your node_modules monorepo is set up with symlinks, you can build packages even without a root rescript.json: ``` my-monorepo/ ├──node_modules/ │ ├── package-1/ # symlinked │ ├── package-2/ # symlinked ├── package.json ├── packages/ │ ├── package-1/ │ │ ├── rescript.json │ │ ├── package.json │ │ ├── src/ │ ├── package-2/ │ │ ├── rescript.json │ │ ├── package.json │ │ ├── src/ │ ├── ... ``` Building `package-2` (which depends on `package-1`) will search up the folder structure to find `package-1`. Example: ```bash rescript build ./packages/package-2 ``` Internally, Rewatch will look for: - 🔴 `my-monorepo/packages/package-2/node_modules/package-1` - 🔴 `my-monorepo/packages/node_modules/package-1` - ✅ `my-monorepo/node_modules/package-1` This only happens as a last resort if `package-1` is not listed as a (dev-)dependency in a parent `rescript.json`. ## Troubleshooting If you're having issues with your monorepo setup, you can use the `-v` flag during build to see what Rewatch detected as the project context: ```bash rescript build -v ``` This will show you detailed information about how Rewatch is interpreting your project structure and which configuration files it's using. ## Recommendation **The ReScript team strongly recommends using a root `rescript.json` file when setting up monorepos.** While it's technically possible to build packages without one (as shown in the section above), having a root configuration file provides several benefits: - **Consistent settings** across all packages (jsx, experimental features, package-specs, suffix) - **Simplified dependency management** through the root dependencies array - **Better developer experience** with unified build commands from the root - **Easier maintenance** and configuration updates across the entire monorepo The root `rescript.json` approach is the intended and supported way to work with ReScript monorepos. # Interop with JS Build Systems If you come from JS, chances are that you already have a build system in your existing project. Here's an overview of the role `rescript` would play in your build pipeline, if you want to introduce some ReScript code. > **Please** try not to wrap `rescript` into your own incremental build framework. ReScript's compilation is very hard to get right, and you'll inevitably run into stale or badly performing builds (therefore erasing much of our value proposition) if you create your own meta layer on top. ## Popular JS Build Systems The JS ecosystem uses a few build systems: [vite](https://vite.dev/), [browserify](http://browserify.org/), [rollup](https://github.com/rollup/rollup), [webpack](https://webpack.js.org/), etc. The first one is probably the most popular of the four (as of 2025). These build systems do both the compilation and the linking (aka, bundling many files into one or few files). `rescript` only takes care of the compilation step; it maps one `.res`/`.resi` file into one JS output file. As such, in theory, no build system integration is needed from our side. From e.g. the webpack watcher's perspective, the JS files ReScript generates are almost equivalent to your hand-written JS files. We also recommend **that you initially check in those ReScript-generated JS files**, as this workflow means: - You can introduce ReScript silently into your codebase without disturbing existing infra. - You have a **visual** diff of the performance & correctness of your JS file when you update the `.res` files and the JS artifacts change. - You can let teammates hot-patch the JS files in emergency situations, without needing to first start learning ReScript. - You can remove ReScript completely from your codebase and things will still work (in case your company decides to stop using us for whatever reason). For what it's worth, you can also turn `rescript` into an automated step in your build pipeline, e.g. into a Webpack loader; but such approach is error-prone and therefore discouraged. ### Tips & Tricks You can make ReScript JS files look even more idiomatic through the in-source + bs suffix config in `rescript.json`: ```json { "package-specs": { "module": "commonjs", // or whatever module system your project uses "in-source": true }, "suffix": ".res.js" } ``` This will: - Generate the JS files alongside your ReScript source files. - Use the file extension `.res.js`, so that you can require these files on the JS side through `require('./MyFile.res.js')`, without needing a loader. ## Use Loaders on ReScript Side "What if my build system uses a CSS/png/whatever loader and I'd like to use it in ReScript?" Loaders are indeed troublesome; in the meantime, please use e.g. `%raw("require('./myStyles.css')")` at the top of your file. This just uses [`raw`](./embed-raw-javascript.mdx) to compile the snippet into an actual JS require. ## Getting Project's Dependencies `rescript` generates one `MyFile.d` file per `MyFile` source file; you'll find them in `lib/bs`. These are human readable, machine-friendly list of the dependencies of said `MyFile`. You can read into them for your purpose (though mind the IO overhead). Use these files instead of creating your own dependency graph; we did the hard work of tracking the dependencies as best as possible (including inner modules, `open`s, module names overlap, etc). ## Run Script Per File Built See [js-post-build](./build-configuration.mdx#js-post-build). Though please use it sparingly; if you hook up a node.js script after each file built, you'll incur the node startup time per file! # Build Performance ReScript considers performance at install time, build time and run time as a serious feature; it's one of those things you don't notice until you realize it's missing. ## Profile Your Build Sometime your build can be slow due to some confused infra setups. We provide an interactive visualization of your build's performance via `bstracing`: ```sh ./node_modules/.bin/bstracing ``` Run the above command at your ReScript project's root; it'll spit out a JSON file you can drag and drop into `chrome://tracing`. ## Under the Hood ReScript itself uses a build system under the hood, called [Ninja](https://ninja-build.org). Ninja is like Make, but cross-platform, minimal, focuses on perf and destined to be more of a low-level building block than a full-blown build system. In this regard, Ninja's a great implementation detail for `rescript`. ReScript reads into `rescript.json` and generates the Ninja build file in `lib/bs`. The file contains the low-level compiler commands, namespacing rules, intermediate artifacts generation & others. It then runs `ninja` for the actual build. ## The JS Wrapper `rescript` itself is a Node.js wrapper which takes care of some miscellaneous tasks, plus the watcher. The lower-level, watcher-less, fast native `rescript` is called `rescript.exe`. It's located at `node_modules/rescript/{your-platform}/rescript.exe`. If you don't need the watcher, you can run said `rescript.exe`. This side-steps Node.js' long startup time, which can be in the order of `100ms`. Our editor plugin finds and uses this native `rescript.exe` for better performance. ## Numbers Raw `rescript.exe` build on a small project should be around `70ms`. This doubles when you use the JS `rescript` wrapper which comes with a watcher, which is practically faster since you don't manually run the build at every change (though you should opt for the raw `rescript.exe` for programmatic usage, e.g. inserting rescript into your existing JS build pipeline). No-op build (when no file's changed) should be around `15ms`. Incremental rebuild (described soon) of a single file in a project is around `70ms` too. Cleaning the artifacts should be instantaneous. ### Extreme Test We've stress-tested `rescript.exe` on a big project of 10,000 files (2 directories, 5000 files each, first 5000 no dependencies, last 5000 10 dependencies on files from the former directory) using https://github.com/rescript-lang/build-benchmark, on a Retina Macbook Pro Early 2015 (3.1 GHz Intel Core i7). {/* TODO: better repro */} - No-op build of 10k files: `800ms` (the minimum amount of time required to check the mtimes of 10k files). - Clean build: \<3 minutes. - Incremental build: depends on the number of the dependents of the file. No dependent means `1s`. ### Stability `rescript` is a file-based build system. We don't do in-memory build, even if that speeds up the build a lot. In-memory builds risk memory leaks, out-of-memory errors, corrupt halfway build and others. Our watcher mode stays open for days or months with no leak. The watcher is also just a thin file watcher that calls `rescript.exe`. We don't like babysitting daemon processes. ## Incrementality & Correctness ReScript doesn't take whole seconds to run every time. The bulk of the build performance comes from incremental build, aka re-building a previously built project when a few files changed. In short, thanks to our compiler and the build system's architecture, we're able to **only build what's needed**. If `MyFile.res` isn't changed, it isn't recompiled. Renaming or moving files is handled automatically, with no stale builds. ## Speed Up Incremental Build ReScript uses the concept of interface files (`.resi`) (or, equivalently, [module signatures](./module.mdx#signatures)). Exposing only what you need naturally speeds up incremental builds. E.g. if you change a `.res` file whose corresponding `.resi` file doesn't expose the changed part, then you've reduced the amount of dependent files you have to rebuild. ## Programmatic Usage Unfortunately, JS build systems are usually the bottleneck for building a JS project nowadays. Having parts of the build blazingly fast doesn't matter much if the rest of the build takes seconds or literally minutes. Here are a few suggestions: - Convert more files into ReScript =). Fewer files going through fewer parts of the JS pipeline helps a ton. - Careful with bringing in more dependencies: libraries, syntax transforms (e.g. the unofficially supported PPX), build step loaders, etc. The bulk of these dragging down the editing & building experience might out-weight the API benefits they provide. ## Hot Reloading Hot reloading refers to maintaining a dev server and listening to file changes in a way that allows the server to pipe some delta changes right into the currently running browser page. This provides a relatively fast iteration workflow while working in specific frameworks. However, hot reloading is fragile by nature, and counts on the occasional inconsistencies (bad state, bad eval, etc.) and the heavy devserver setup/config being less of a hassle than the benefits it provides. We err on the side of caution and stability in general, and decided not to provide a built-in hot reloading _yet_. **Note**: you can still use the hot reloading facility provided by your JS build pipeline. # Warning Numbers You can configure which warnings the ReScript compiler generates [in the build configuration](./build-configuration.mdx#warnings) or using the [`@warning()`](../../syntax-lookup/decorator_expression_warning.mdx) or the [`@@warning()`](../../syntax-lookup/decorator_module_warning.mdx) decorator.