Introduction to Dune
Depending on how you’ve been following along, you may have several components in your project. Since these components don’t have much in common with each other, it makes sense to put them in separate, independent single-page apps. To do that, we’ll use Dune, a build system designed for OCaml projects, with many useful features. For our purposes, the feature of primary interest is its built-in support for Melange.
dune-project file
The dune-project file specifies metadata for a project, and should appear in the root directory of your project. If you’ve been using the starter project, then you’ve been using Dune this whole time and therefore already have a dune-project file:
(lang dune 3.8)
; Use version 0.1 of the melange plugin for dune
(using melange 0.1)
; Set the name which is used by error messages
(name melange-for-react-devs)
; Copy all build targets for an alias into the sandbox
(expand_aliases_in_sandbox)(lang dune 3.8)
; Use version 0.1 of the melange plugin for dune
(using melange 0.1)
; Set the name which is used by error messages
(name melange-for-react-devs)
; Copy all build targets for an alias into the sandbox
(expand_aliases_in_sandbox)The line
(using melange 0.1)(using melange 0.1)is necessary because we have to manually enable the Melange extension for Dune in order to use it. Note that the version of the Melange Dune extension is independent of the version of Melange we’re using.
Technically, dune-project accepts many more metadata fields, but it’s best to keep it minimal. Other than name, it makes more sense to put the rest of your project’s metadata fields in your .opam file, which we’ll cover later.
dune-project files use S-expressions, which might make you think of the Lisp programming language. However, S-expressions are just a convenient syntax for encoding structured data, and Dune doesn’t have the power of a full scripting language.
Each S-expression at the top level is a known as a stanza. All the possible stanzas you can use in dune-project can be found in Dune’s Stanza Reference.
dune files
Besides dune-project, Dune also looks at the dune files in our project. Basically, dune files tell Dune about directories, executables, libraries, tests, and anything else of interest. For example, here’s the dune file inside the root directory of your project:
; `dirs` is a stanza to tell dune which subfolders from the current folder
; (where the `dune` file is) it should process. Here it is saying to include
; all directories that don't start with . or _, but exclude node_modules.
(dirs :standard \ node_modules)
; `melange.emit` is a Dune stanza that will produce build rules to generate
; JavaScript files from sources using the Melange compiler
; https://dune.readthedocs.io/en/stable/melange.html#melange-emit
(melange.emit
; The `target` field is used by Dune to put all JavaScript artifacts in a
; specific folder inside `_build/default`
(target output)
; Here's the list of dependencies of the stanza. In this case (being
; `melange.emit`), Dune will look into those dependencies and generate rules
; with JavaScript targets for the modules in those libraries as well.
; Caveat: the libraries need to be specified with `(modes melange)`.
(libraries reason-react)
; The `preprocess` field lists preprocessors which transform code before it is
; compiled. melange.ppx allows to use Melange attributes [@mel. ...]
; (https://melange.re/v4.0.0/communicate-with-javascript.html#attributes)
; reason-react-ppx allows to use JSX for ReasonReact components by using the
; [@JSX] attributes from Reason: https://reasonml.github.io/docs/en/jsx
(preprocess
(pps melange.ppx reason-react-ppx))
; module_systems lets you specify commonjs (the default) or es6
(module_systems es6)); `dirs` is a stanza to tell dune which subfolders from the current folder
; (where the `dune` file is) it should process. Here it is saying to include
; all directories that don't start with . or _, but exclude node_modules.
(dirs :standard \ node_modules)
; `melange.emit` is a Dune stanza that will produce build rules to generate
; JavaScript files from sources using the Melange compiler
; https://dune.readthedocs.io/en/stable/melange.html#melange-emit
(melange.emit
; The `target` field is used by Dune to put all JavaScript artifacts in a
; specific folder inside `_build/default`
(target output)
; Here's the list of dependencies of the stanza. In this case (being
; `melange.emit`), Dune will look into those dependencies and generate rules
; with JavaScript targets for the modules in those libraries as well.
; Caveat: the libraries need to be specified with `(modes melange)`.
(libraries reason-react)
; The `preprocess` field lists preprocessors which transform code before it is
; compiled. melange.ppx allows to use Melange attributes [@mel. ...]
; (https://melange.re/v4.0.0/communicate-with-javascript.html#attributes)
; reason-react-ppx allows to use JSX for ReasonReact components by using the
; [@JSX] attributes from Reason: https://reasonml.github.io/docs/en/jsx
(preprocess
(pps melange.ppx reason-react-ppx))
; module_systems lets you specify commonjs (the default) or es6
(module_systems es6))Like dune-project, a dune file consists of one or more stanzas. The first stanza is dirs, which tells Dune which directories to include in the build. Note that the stanzas accepted in dune files are not the same as the ones accepted by dune-project. See all possible dune stanzas in Dune’s Stanza Reference.
melange.emit stanza
The main stanza of interest for us is melange.emit, which tells Dune to turn our OCaml files into JavaScript files. The fields we give to melange.emit here are target, libraries, preprocess, and module_systems, which are ones that we need to use for pretty much every Melange project.
melange.emit tells Dune to compile Index.re in your root directory to Index.js somewhere in the _build/default directory. The specific location of Index.js depends on the location of the dune file and the value of melange.emit’s target field. Our dune file is in the root directory and target’s value is output, so the location of Index.js is
_build/default/output/Index.js_build/default/output/Index.jsWe need to know the exact location of Index.js so that we can reference it in the index.html file in the root directory:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Melange for React Developers</title>
<script type="module" src="./_build/default/output/Index.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Melange for React Developers</title>
<script type="module" src="./_build/default/output/Index.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>For more details about where JavaScript output files end up in the build directory, see JavaScript artifacts layout.
TIP
In this dune file, we’re only concerned with building JavaScript to run in the browser, but if we also wanted to build JavaScript to run on Node we could include another melange.emit stanza specifically for that. See melange-opam-template for an example of building for Node.
Counter app directory
Create a new directory src/counter, which will contain a new app that only renders the Counter component. Then make sure this new directory contains:
Counter.re(move from root directory tosrc/counter)dunefile that just has amelange.emitstanzadune; `melange.emit` is a Dune stanza that will produce build rules to generate ; JavaScript files from sources using the Melange compiler ; https://dune.readthedocs.io/en/stable/melange.html#melange-emit (melange.emit ; The `target` field is used by Dune to put all JavaScript artifacts in a ; specific folder inside `_build/default` (target output) ; Here's the list of dependencies of the stanza. In this case (being ; `melange.emit`), Dune will look into those dependencies and generate rules ; with JavaScript targets for the modules in those libraries as well. ; Caveat: the libraries need to be specified with `(modes melange)`. (libraries reason-react) ; The `preprocess` field lists preprocessors which transform code before it is ; compiled. These enable, for example, the use of JSX in .re files. (preprocess (pps melange.ppx reason-react-ppx)) ; module_systems lets you specify commonjs (the default) or es6 (module_systems es6)); `melange.emit` is a Dune stanza that will produce build rules to generate ; JavaScript files from sources using the Melange compiler ; https://dune.readthedocs.io/en/stable/melange.html#melange-emit (melange.emit ; The `target` field is used by Dune to put all JavaScript artifacts in a ; specific folder inside `_build/default` (target output) ; Here's the list of dependencies of the stanza. In this case (being ; `melange.emit`), Dune will look into those dependencies and generate rules ; with JavaScript targets for the modules in those libraries as well. ; Caveat: the libraries need to be specified with `(modes melange)`. (libraries reason-react) ; The `preprocess` field lists preprocessors which transform code before it is ; compiled. These enable, for example, the use of JSX in .re files. (preprocess (pps melange.ppx reason-react-ppx)) ; module_systems lets you specify commonjs (the default) or es6 (module_systems es6))Index.reto render the app to the DOMremodule App = { [@react.component] let make = () => <div> <h1> {React.string("Counter")} </h1> <Counter /> </div>; }; let node = ReactDOM.querySelector("#root"); switch (node) { | None => Js.Console.error("Failed to start React: couldn't find the #root element") | Some(root) => let root = ReactDOM.Client.createRoot(root); ReactDOM.Client.render(root, <App />); };module App = { [@react.component] let make = () => <div> <h1> {React.string("Counter")} </h1> <Counter /> </div>; }; let node = ReactDOM.querySelector("#root"); switch (node) { | None => Js.Console.error("Failed to start React: couldn't find the #root element") | Some(root) => let root = ReactDOM.Client.createRoot(root); ReactDOM.Client.render(root, <App />); };index.htmlis the page for your new app and tells Vite where to findIndex.js:html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Melange for React Devs</title> <script type="module" src="../../_build/default/src/counter/output/src/counter/Index.js"></script> </head> <body> <div id="root"></div> </body> </html><!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Melange for React Devs</title> <script type="module" src="../../_build/default/src/counter/output/src/counter/Index.js"></script> </head> <body> <div id="root"></div> </body> </html>
After you’ve added those files, your src/counter directory should look like this:
src/counter
├─ Counter.re
├─ dune
├─ Index.re
└─ index.htmlsrc/counter
├─ Counter.re
├─ dune
├─ Index.re
└─ index.htmlStructure of _build directory
If you look inside the _build/default/src/counter directory, you’ll see that your src/counter directory is essentially mirrored there, along with some extra files:
_build/default/src/counter
├─ .merlin-conf/
├─ .output.mobjs/
├─ output/
├─ Counter.re
├─ Counter.re.ml
├─ Counter.re.pp.ml
├─ Index.re
├─ Index.re.ml
└─ Index.re.pp.ml_build/default/src/counter
├─ .merlin-conf/
├─ .output.mobjs/
├─ output/
├─ Counter.re
├─ Counter.re.ml
├─ Counter.re.pp.ml
├─ Index.re
├─ Index.re.ml
└─ Index.re.pp.mlExtra files and directories like .output.mobjs and Counter.re.ml are build artifacts and we won’t go into any detail about them. If you look inside _build/default/src/counter/output/src/counter, you’ll see that src/counter is mirrored there as well, but this time the directory only contains the generated .js files:
_build/default/src/counter/output/src/counter
├─ .output.mobjs/
├─ Counter.js
└─ Index.js_build/default/src/counter/output/src/counter
├─ .output.mobjs/
├─ Counter.js
└─ Index.jsSo the _build directory contains two mirrored directories for src/counter:
_build/default/src/counter, which contains copies of the.resource files and their intermediate build artifacts_build/default/src/counter/output/src/counter, which contains the final generated.jsfiles.
Update root directory index.html
Now that we’ve added src/counter/index.html, we don’t need the root directory’s index.html to render the Counter app. Instead, it can serve as an index page to link to all of our single-page apps. Change index.html to this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Melange for React Developers</title>
</head>
<body>
<h1>Melange for React Developers</h1>
<ul>
<li>
<a href="src/counter/index.html">Counter</a>
</li>
</ul>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Melange for React Developers</title>
</head>
<body>
<h1>Melange for React Developers</h1>
<ul>
<li>
<a href="src/counter/index.html">Counter</a>
</li>
</ul>
</body>
</html>Now run npm run serve to check that everything works as expected.
TIP
Feel free to do a little cleanup before moving on:
- Delete the
Index.refile in the root directory - Delete the
melange.emitstanza from the root directory’sdunefile
Rationale for monorepo structure
Going forward, we’re going to use a monorepo where projects are separate apps that are developed in their own directory. Each project directory will have its own dune file with its own melange.emit stanza. We want to use a monorepo because most projects will have very similar dependencies, so it seems overkill to create new dune-project, .opam, and package.json files[1] for every single project.
INFO
Melange documentation’s guidelines for melange.emit recommends you put the melange.emit stanza in the dune file in the project’s root directory. We are no longer doing that going forward, but this is still great advice if your repo only contains a single app!
Huzzah! You created a new dune file to build an app and created an index page for all your apps. In future chapters, we assume that you will use the same directory structure for each new app you build.
Overview
- Dune is the build system we use to build Melange projects
- The
dune-projectfile describes the metadata for your project, primarily:- The version of Dune you’re using
- The version of the Melange plugin for Dune you’re using
- The name of your project
dunefiles describe things of interest to Dune, for example:- Which directories to include and which to exclude
- Which directories contain code that should be transpiled to JavaScript, using the
melange.emitstanza
Exercises
1. Repeat the steps we did for Counter and create a separate app for Celsius Converter.
Solution
Creating a separate app for Celsius Converter, with its own dune, Index.re, and index.html files, should look something like this. For ease of navigation, you should also update index.html in the root directory:
<li>
<a href="src/counter/index.html">Counter</a>
</li>
<li>
<a href="src/celsius-converter-exception/index.html">Celsius Converter</a>
</li><li>
<a href="src/counter/index.html">Counter</a>
</li>
<li>
<a href="src/celsius-converter-exception/index.html">Celsius Converter</a>
</li>2. Delete reason-react-ppx from src/counter/dune’s melange.emit stanza. What compiler errors do you get?
(preprocess
(pps melange.ppx reason-react-ppx))
(pps melange.ppx)) (preprocess
(pps melange.ppx reason-react-ppx))
(pps melange.ppx)) Solution
The compilation error will be:
File "src/counter/Counter.re", line 5, characters 2-6:
5 | <div
^^^^
Error: Unbound value divFile "src/counter/Counter.re", line 5, characters 2-6:
5 | <div
^^^^
Error: Unbound value divThat’s because putting reason-react-ppx in the preprocess/pps field will transform function calls to div (which isn’t defined anywhere) into calls to React.createElement("div", ...)[2].
3. Assume you have a directory foo/bar in the root of your project directory with these files in it:
foo/bar
├─ Hello.re
└─ dunefoo/bar
├─ Hello.re
└─ duneThe contents of foo/bar/dune are:
(melange.emit
(target dist)
(libraries reason-react)
(preprocess
(pps melange.ppx reason-react-ppx))
(module_systems es6))(melange.emit
(target dist)
(libraries reason-react)
(preprocess
(pps melange.ppx reason-react-ppx))
(module_systems es6))What is the path of the Hello.js file generated by Melange?
Solution
The source file foo/bar/Hello.re would produce a .js file at:
_build/default/foo/bar/dist/foo/bar/Hello.js_build/default/foo/bar/dist/foo/bar/Hello.jsSource code for this chapter: