JavaScript Compilation With Melange

Introduction

Melange compiles OCaml to JavaScript. It produces one JavaScript file per OCaml module. Melange can be installed with opam:

$ opam install melange

Dune can build Melange projects, and produces JavaScript files by defining a melange.emit stanza. Dune libraries may also define Melange libraries by adding melange to (modes ...) in the library stanza.

Melange support must be enabled in the dune-project file:

(using melange 1.0)

Once that’s in place, you can use the Melange mode in library and melange.emit stanzas.

Simple Project

Let’s start by looking at a simple project with Melange and Dune. Subsequent sections explain the different concepts used here in further detail.

First, make sure that the dune-project file specifies at least version 3.20 of the Dune language, and the Melange extension is enabled:

(lang dune 3.26)
(using melange 1.0)

Next, write a dune file with a melange.emit stanza:

(melange.emit
 (target output))

Finally, add a source file to build:

$ echo 'Js.log "hello from melange"' > hello.ml

After running dune build @melange or just dune build, Dune produces the following file structure:

.
├── _build
│   └── default
│       └── output
│           └── hello.js
├── dune
├── dune-project
└── hello.ml

The resulting JavaScript can now be run:

$ node _build/default/output/hello.js
hello from melange

Libraries

Adding Melange support to Dune libraries is done as follows:

  • (modes melange): adding melange to modes is required. This field also supports the Ordered Set Language.

  • (melange.runtime_deps <deps>): optionally, define any runtime dependencies using melange.runtime_deps. This field is analog to the runtime_deps field used in melange.emit stanzas.

  • (melange.compile_flags <flags>): optionally, pass flags to melc compiler. This field is analogous to the compile_flags field used in melange.emit stanzas.

Building Without Melange

Added in version 3.25.

A library can support Melange without requiring it in every opam switch. Declare both OCaml and Melange modes, for example:

(library
 (public_name portable)
 (modes :standard melange))

If Dune cannot find the Melange compiler, melc, in the build context, it skips the Melange variant of this library. The OCaml variants can still be built.

When melc is available, the library retains both OCaml and Melange modes. Libraries installed without that variant must be rebuilt and reinstalled with melc available before they can be used by Melange consumers.

This behavior is available with Dune 3.25 and later.

There are two important limits:

  • A library with only (modes melange) still requires melc; it is not silently skipped when the compiler is missing.

  • Building the outputs of a melange.emit stanza still requires melc. This includes @all when it selects those outputs. Merely declaring the stanza does not prevent building an unrelated OCaml target without Melange.

The OCaml variants must still have all their dependencies and preprocessors available. This behavior does not make an unconditional melange dependency in an opam package optional.

melange.emit

Added in version 3.8.

The melange.emit stanza produces JavaScript files from Melange libraries or entry-point modules. It’s similar to the OCaml executable stanza, with the exception that there is no linking step.

(melange.emit
 (target <target>)
 <optional-fields>)
  • <target> is the name of the folder inside the build directory where Dune will compile the resulting JavaScript. In particular, the folder will be placed under _build/default/$path-to-directory-of-melange-emit-stanza.

    Note: when using promotion, Dune will additionally copy the resulting JavaScript back to the source tree, next to the original source files.

$path-to-directory-of-melange-emit-stanza matches the file structure of the source tree. For example, given the following source tree:

├── dune # (melange.emit (target output) (libraries lib))
├── app.ml
└── lib
    ├── dune # (library (name lib) (modes melange))
    └── helper.ml

The resulting layout in _build/default/output will be as follows:

output
├── app.js
└── lib
    ├── lib.js
    └── helper.js

Artifact Variable

Added in version 3.25.

The %{melange.emit:<target-dir>} variable expands to the output directory of the selected melange.emit stanza. <target-dir> is the path to the stanza’s target directory, relative to the dune file containing the variable. Like other artifact variables, it adds a dependency on the stanza’s outputs.

For example, suppose lib/dune contains a stanza with (target output). In that file, %{melange.emit:output} expands to output/lib. In a dune file at the workspace root, %{melange.emit:lib/output} expands to lib/output/lib.

<optional-fields> are:

  • (alias <alias-name>) specifies an alias to which to attach the targets of the melange.emit stanza.

    • These targets include the .js files generated by the stanza modules, the targets for the .js files of any library that the stanza depends on, and any copy rules for runtime dependencies (see runtime_deps field below).

    • By default, all stanzas will have their targets attached to an alias melange. The behavior of this default alias is exclusive: if an alias is explicitly defined in the stanza, the targets from this stanza will be excluded from the melange alias.

    • The targets of melange.emit are also attached to the Dune default alias (@all), regardless of whether the (alias ...) field is present.

  • (module_systems <module_systems>) specifies the JavaScript import and export format used. The values allowed for <module_systems> are es6 and commonjs.

    • es6 will follow JavaScript modules, and will produce import and export statements.

    • commonjs will follow CommonJS modules, and will produce require calls and export values with module.exports.

    • If no extension is specified, the resulting JavaScript files will use .js. You can specify a different extension with a pair (<module_system> <extension>), e.g. (module_systems (es6 mjs)).

    • Multiple module systems can be used in the same field as long as their extensions are different. For example, (module_systems commonjs (es6 mjs)) will produce one set of JavaScript files using CommonJS and the .js extension, and another using ES6 and the .mjs extension.

  • (modules <modules>) specifies what modules will be built with Melange. By default, if this field is not defined, Dune will use all the .ml/.re files in the same directory as the dune file. This includes module sources present in the file system as well as modules generated by user rules. You can restrict this list by using an explicit (modules <modules>) field. <modules> uses the Ordered Set Language, where elements are module names and don’t need to start with an uppercase letter. For instance, to exclude module Foo, use (modules :standard \ foo).

  • (libraries <library-dependencies>) specifies Melange library dependencies. Melange libraries can only use the simple form, like (libraries foo pkg.bar). Keep in mind the following limitations:

    • The re_export form is not supported.

    • All the libraries included in <library-dependencies> have to support the melange mode (see the section about libraries below).

  • (package <package>) allows the user to define the JavaScript package to which the artifacts produced by the melange.emit stanza will belong.

  • (runtime_deps <paths-to-deps>) specifies dependencies that should be copied to the build folder together with the .js files generated from the sources. These runtime dependencies can include assets like CSS files, images, fonts, external JavaScript files, etc. runtime_deps adhere to the formats in Dependency Specification. For example (runtime_deps ./path/to/file.css (glob_files_rec ./fonts/*)).

  • (emit_stdlib <bool>) allows the user to specify whether the Melange standard library should be included as a dependency of the stanza or not. The default is true. If this option is false, the Melange standard library and runtime JavaScript files won’t be produced in the target directory.

  • (promote <options>) promotes the generated .js files to the source tree. The options are the same as for the rule promote mode. Adding (promote (until-clean)) to a melange.emit stanza will cause Dune to copy the .js files to the source tree and dune clean to delete them. Check Promotion for more details.

  • (preprocess <preprocess-spec>) specifies how to preprocess files when needed. The default is no_preprocessing. Additional options are described in the Preprocessing Specification section.

  • (lint <preprocess-spec>) specifies how to lint source files when building the @lint alias. The default is no_preprocessing. The syntax is described in Linting.

  • (preprocessor_deps (<deps-conf list>)) specifies extra preprocessor dependencies, e.g., if the preprocessor reads a generated file. The dependency specification is described in the Dependency Specification section.

  • (compile_flags <flags>) specifies compilation flags specific to melc, the main Melange executable. <flags> is described in detail in the Ordered Set Language section. It also supports (:include ...) forms. The value for this field can also be taken from env stanzas. It’s therefore recommended to add flags with e.g. (compile_flags :standard <my options>) rather than replace them.

  • (root_module <module>) specifies a root_module that collects all listed dependencies in libraries. See the documentation for root_module in the library stanza.

  • (allow_overlapping_dependencies) is the same as the corresponding field of library.

  • (enabled_if <blang expression>) conditionally disables a melange emit stanza. The JavaScript files associated with the stanza won’t be built. The condition is specified using the Boolean Language.

Promotion

Compiling and promoting Melange output in Dune is slightly different than compiling OCaml:

  • Limitations in Dune rule production require a target directory in melange.emit.

    • The target directory is total: it can be exported as is from the Dune build directory

  • Many popular tools and frameworks in the JavaScript ecosystem today rely on convention over configuration, especially as it relates to folder structure. When using promotion

Design choices

Melange support in Dune follows the following design choices:

  • melange.emit produces a “total” directory: the artifacts in the target directory contain all the JavaScript and runtime_deps assets necessary to run the application either through a JS framework, a bundler, or otherwise a deployment (excluding external dependencies installed via a JS package manager). The structure is designed such that relative paths and dependencies work out of the box relative to their paths in the source tree, before compilation.

  • public libraries are compiled to %{target}/node_modules/%{lib_name} such that the resolution algorithm works to resolve Melange libraries from compiled JS code.

  • JavaScript output is promoted to the source tree