How-to guides

Migrate a ReScript library to Melange

It is possible to use existing ReScript (formerly BuckleScript) code with Melange, mostly as is. However, as both projects evolve in different directions over time, it will become more challenging to do so as time goes by, as some of the most recent features of ReScript might not be directly convertible to make them work with Melange.

For this reason, the recommendation is to migrate libraries at a time where they were compatible with past versions of ReScript, for example v9 (or v10 at most).

These are the steps to follow:

  • Add an opam file
  • Add a dune-project file
  • Replace the bsconfig.json file with one or multiple dune files
  • (Optional) Migrate from ReScript syntax to Reason or OCaml syntaxes
  • Make sure everything works: dune build
  • Final step: remove bsconfig.json and adapt package.json

Let's go through them in detail:

Add an opam file

To migrate your ReScript library to Melange, you will need some packages. Melange is designed to be used with opam, the package manager of OCaml, which is explained in its own section.

To get started with the library migration, let's create an opam file in your library's root folder with the minimum set of packages to start working:

opam-version: "2.0"
synopsis: "My Melange library"
description: "A library for Melange"
maintainer: ["<your_name>"]
authors: ["<your_name>"]
license: "XXX"
homepage: "https://github.com/your/project"
bug-reports: "https://github.com/your/project/issues"
depends: [
  "ocaml"
  "dune"
  "melange"
]
dev-repo: "git+https://github.com/your/project.git"

If your library was using Reason syntax (re files), you will need to add "reason" to the list of dependencies. If the library was using ReScript syntax (res files), you will need to add rescript-syntax to the list of dependencies. You can read more about how to migrate from ReScript syntax in the section below.

At this point, we can create a local opam switch to start working on our library:

opam switch create . 5.1.0~rc2 -y --deps-only

Once this step is done, we can call dune from the library folder, but first we need some configuration files.

Add a dune-project file

Create a file named dune-project in the library root folder. This file will tell Dune a few things about our project configuration:

(lang dune 3.8)

(using melange 0.1)

Replace the bsconfig.json file with one or multiple dune files

Now, we need to add a dune file where we will tell Dune about our library. You can put this new file next to the library sources, it will look something like this:

(library
 (name things)
 (modes melange))

Let's see how the most common configurations in bsconfig.json (or rescript.json) map to dune files. You can find more information about these configurations in the Rescript docs and in the Dune docs.

name, namespace

These two configurations map to Dune (wrapped <boolean>) field in the library stanza. By default, all Dune libraries are wrapped, which means that a single module with the name of the library is exposed at the top level. So e.g. of your bsconfig.json had "namespace": false, you can add (wrapped false) to your library, although wrapped libraries are heavily encouraged to avoid global namespace pollution.

It's important to note that the permissible character range for naming conventions differs between ReScript namespaces and Dune libraries. Dune library names must adhere to the naming criteria set for OCaml modules. For instance, if your bsconfig.json configuration includes a naming scheme like this:

{
  "namespace": "foo-bar"
}

It should be converted into something like:

(library
 (name fooBar) # or (name foo_bar)
 (modes melange))

sources

Dune works slightly differently than ReScript when it comes down to including source folders as part of a library.

By default, when Dune finds a dune file with a library stanza, it will include just the files inside that folder to the library itself (unless the modules field is used). If you want to create a library with multiple subfolders in it, you can use the following combination of stanzas:

  • (include_subdirs unqualified) (docs): This stanza tells Dune to look for sources in all the subfolders of the folder where the dune file lives.
  • (dirs foo bar) (docs): This stanza tells Dune to only look into foo and bar subdirectories of the current folder.

So for example, if your library had this configuration in its bsconfig.json:

{
  "sources": ["src", "helper"]
}

You might translate this to a dune file with the following configuration:

(include_subdirs unqualified)
(dirs src helper)
(library
 (name things)
 (modes melange))

Alternatively, depending on the case, you could place two separate dune files, one in src and one in helper, and define one library on each. In that case, include_subdirs and dirs would not be necessary.

Regarding the "type" : "dev" configuration in ReScript, the way Dune solves that is with public and private libraries. If a library stanza includes a public_name field, it becomes a public library, and will be installed. Otherwise it is private and won't be visible by consumers of the package.

bs-dependencies

Your library might depend on other libraries. To specify dependencies of the library in the dune file, you can use the libraries field of the library stanza.

For example, if bsconfig.json had something like this:

"bs-dependencies": [
  "reason-react"
]

Your dune file will look something like:

(library
 (name things)
 (libraries reason-react)
 (modes melange))

Remember that you will have to add all dependencies to your library opam package as well.

bs-dev-dependencies

Most of the times, bs-dev-dependencies is used to define dependencies required for testing. For this scenario, opam provides the with-test variable.

Supposing we want to add melange-jest as a dependency to use for tests, you could add this in your library opam file:

depends: [
  "melange-jest" {with-test}
]

The packages marked with this variable become dependencies when opam install is called with the --with-test flag.

Once the library melange-jest has been installed by opam, it is available in Dune, so adding (libraries melange-jest) to your library or melange.emit stanzas would be enough to start using it.

pinned-dependencies

Dune allows to work inside monorepos naturally, so there is no need for pinned dependencies. Packages can be defined in the dune-project file using the packages stanza, and multiple dune-project files can be added across a single codebase to work in a monorepo setup.

external-stdlib

There is no direct mapping of this functionality in Melange. If you are interested in it, or have a use case for it, please share with us on issue melange-re/melange#620.

js-post-build

You can use Dune rules to perform actions, that produce some targets, given some dependencies.

For example, if you had something like this in bsconfig.json:

{
  "js-post-build": {
    "cmd": "node ../../postProcessTheFile.js"
  }
}

This could be expressed in a dune file with something like:

(rule 
  (deps (alias melange))
  (action (run node ../../postProcessTheFile.js))
)

To read more about Dune rules, check the documentation.

package-specs

This setting is not configured at the library level, but rather at the application level, using the module_systems field in the melange.emit stanza. To read more about it, check the corresponding build system section.

Regarding the "in-source" configuration, the corresponding field in Dune would be the (promote (until-clean)) configuration, which can be added to a melange.emit stanza. You can read more about it in the Dune documentation.

suffix

Same as with package-specs this configuration is set at the application level, using the module_systems field in the melange.emit stanza. Check the CommonJS or ES6 modules section to learn more about it.

warnings and bsc-flags

You can use the flags field of the library stanza to define flags to pass to Melange compiler.

If you want to define flags only for Melange, you can use melange.compile_flags.

For example, if you had a bsconfig.json configuration like this:

{
  "warnings": {
    "number": "-44-102",
    "error": "+5"
  }
}

You can define a similar configuration in your library dune file like this:

(library
 (name things)
 (modes melange)
 (melange.compile_flags :standard -w +5-44-102))

The same applies to bsc-flags.

(Optional) Migrate from ReScript syntax to Reason or OCaml syntax

The package rescript-syntax allows to translate res source files to ml.

To use this package, we need to install it first:

opam install rescript-syntax

Note that the rescript-syntax package is only compatible with the version 1 of melange, so if you are using a more recent version of melange, you might need to downgrade it before installing rescript-syntax.

To convert a res file to ml syntax:

rescript_syntax myFile.res -print ml > myFile.ml

You can use this command in combination with find to convert multiple files at once:

find src1 src2 -type f -name "*.res" -exec echo "rescript_syntax {} -print ml" \;

If you want to convert the files to Reason syntax (re), you can pipe the output of each file to refmt.

rescript_syntax ./myFile.res -print ml | refmt --parse=ml --print re > myFile.re

Note that refmt is available in the reason package, so if your library modules are written using Reason syntax, remember to install it first using opam install reason before performing the conversion, and also adding it to your library opam file as well.

Make sure everything works: dune build

Once you have performed the above steps, you can test that everything works by running

dune build

Throughout the process, you might run into some errors, these are the most common ones:

Warning 16 [unerasable-opt-argument] is triggered more often than before

Melange triggers Warning 16: this optional argument cannot be erased more often than before, as the type system in OCaml 4.12 was improved. You can read more about this in this OCaml PR.

Fix: either add () as final param of the function, or replace one labelled arg with a positional one.

Warning 69 [unused-field] triggered from bindings types

Sometimes, types for bindings will trigger Warning 69 [unused-field]: record field foo is never read. errors.

Fix: silence the warning in the type definition, e.g.

type renderOptions = { 
  foo : string
} [@@warning "-69"]

Destructuring order is changed

Destructuring in let patterns in Melange is done on the left side first, while on ReScript is done on the right side first. You can read more in the Melange PR with the explanation.

Fix: move module namespacing to the left side of the let expressions.

Pervasives is deprecated

This is also another change due to OCaml compiler moving forward.

Fix: Use Stdlib instead.

Runtime assets are missing

In ReScript, building in source is very common. In Melange and Dune, the most common setup is having all artifacts inside the _build folder. If your library is using some asset such as:

external myImage : string = "default" [@@bs.module "./icons/overview.svg"]

Fix: You can include it by using the melange.runtime_deps field of the library:

(library
 (name things)
 (modes melange)
 (melange.runtime_deps icons/overview.svg))

You can read more about this in the Handling assets section.

Final step: remove bsconfig.json and adapt package.json

If everything went well, you can remove the bsconfig.json file, and remove any dependencies needed by Melange from the package.json, as they will be appearing in the opam file instead, as it was mentioned in the bs-dependencies section.