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
opamfile - Add a
dune-projectfile - Replace the
bsconfig.jsonfile with one or multipledunefiles - (Optional) Migrate from ReScript syntax to Reason or OCaml syntaxes
- Make sure everything works:
dune build - Final step: remove
bsconfig.jsonand adaptpackage.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 thedunefile lives.(dirs foo bar)(docs): This stanza tells Dune to only look intofooandbarsubdirectories 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-syntaxpackage is only compatible with the version 1 ofmelange, so if you are using a more recent version ofmelange, you might need to downgrade it before installingrescript-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.