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 projects using Melange, and it allows the user to produce
JavaScript files by defining a melange.emit stanza. Dune libraries can be
used with Melange by adding melange
to (modes ...)
in the
library stanza.
Melange support is still experimental in Dune and needs to be enabled in the dune-project file:
(using melange 0.1)
Once that’s in place, you can use the Melange mode in library stanzas
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.8 of the dune language and the Melange extension is enabled:
(lang dune 3.11)
(using melange 0.1)
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)
: addingmelange
tomodes
is required. This field also supports the Ordered Set Language.(melange.runtime_deps <deps>)
: optionally, define any runtime dependencies usingmelange.runtime_deps
. This field is analog to theruntime_deps
field used inmelange.emit
stanzas.
melange.emit¶
New in version 3.8.
The melange.emit
stanza allows the user to produce JavaScript files
from Melange libraries and 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 where resulting JavaScript artifacts will
be placed. In particular, the folder will be placed under
_build/default/$path-to-directory-of-melange-emit-stanza
.
The result of building a melange.emit
stanza will match 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
<optional-fields>
are:
(alias <alias-name>)
specifies an alias to which to attach the targets of themelange.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 (seeruntime_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 themelange
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>
arees6
andcommonjs
.es6
will follow JavaScript modules, and will produceimport
andexport
statements.commonjs
will follow CommonJS modules, and will produce require calls and export values withmodule.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 thedune
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 moduleFoo
, 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 themelange
mode (see the section about libraries below).
(package <package>)
allows the user to define the JavaScript package to which the artifacts produced by themelange.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 istrue
. If this option isfalse
, 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 amelange.emit
stanza will cause Dune to copy the.js
files to the source tree anddune clean
to delete them.(preprocess <preprocess-spec>)
specifies how to preprocess files when needed. The default isno_preprocessing
. Additional options are described in the Preprocessing Specification section.(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 tomelc
, 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 fromenv
stanzas. It’s therefore recommended to add flags with e.g.(compile_flags :standard <my options>)
rather than replace them.(root_module <module>)
specifies aroot_module
that collects all listed dependencies inlibraries
. See the documentation forroot_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.
Recommended Practices¶
Keep Bundles Small by Reducing the Number of melange.emit
Stanzas¶
It is recommended to minimize the number of melange.emit
stanzas
that a project defines: using multiple melange.emit
stanzas will cause
multiple copies of the JavaScript files to be generated if the same libraries
are used across them. As an example:
(melange.emit
(target app1)
(libraries foo))
(melange.emit
(target app2)
(libraries foo))
The JavaScript artifacts for library foo
will be emitted twice in the
_build
folder. They will be present under _build/default/app1
and _build/default/app2
.
This can have unexpected impact on bundle size when using tools like Webpack or
Esbuild, as these tools will not be able to see shared library code as such,
as it would be replicated across the paths of the different stanzas
target
folders.
Faster Builds With subdir
and dirs
Stanzas¶
Melange libraries might be installed from the npm
package repository,
together with other JavaScript packages. To avoid having Dune inspect
unnecessary folders in node_modules
, it is recommended to explicitly
include only the folders that are relevant for Melange builds.
This can be accomplished by combining subdir and dirs
stanzas in a dune
file next to the node_modules
folder. The
vendored_dirs stanza can be used to avoid warnings in Melange
libraries during the application build. The data_only_dirs stanza
can be useful as well if you need to override the build rules in one of the
packages.
(subdir
node_modules
(vendored_dirs reason-react)
(dirs reason-react))