How to get a melange universal library to work?

Hi everyone,

I am an OCaml beginner and I’m currently implementing a proof-of-concept full-stack web application with Melange. I’m using dune 3.18.0.

The frontend (see lib/frontend) is an Elm-like application using melange-tea because I like Elm. It displays a button and when clicked queries a list of JSON objects from the backend. The backend (see lib/backend) is written with dream. Both frontend and backend define type Animal.t for data exchange. I’m using melange-json (and melange-json-native) to derive JSON encoders and decoders on both the frontend and backend. Also, I’m using esbuild and dune’s Resource bundling mechanism to generate a single Javascript file and bundle it with the executable (see lib/backend/dune).

So far so good. It was a relatively pleasant learning experience.

As a next step, I wanted to move the Animal.t type and the JSON encoding / decoding into a shared (a.k.a. universal) library, see branch shared-library. This is where I got stuck.

I read the “A Preview of Universal Libraries in Dune” article, so I wrote lib/shared/dune like this:

(library
 (name shared)
 (libraries melange-json-native)
 (modules animal)
 (preprocess (pps melange-json-native.ppx))
 (enabled_if
  (= %{context_name} default)))

(library
 (name shared)
 (libraries melange-json)
 (modules animal)
 (modes melange)
 (preprocess (pps melange.ppx melange-json.ppx))
 (enabled_if
  (= %{context_name} melange)))

I got the following compiler error:

Error: The library `shared` was added as a dependency of a `melange.emit`
stanza, but this library is not compatible with Melange. To fix this, add
`melange` to the `modes` field of the library `shared`.

It seems like the melange.emit stanza is executed in the default context where only the first definition of the shared library is visible, and that one does not define (modes melange).

The next thing I tried was to add the enabled_if flag to melange.emit too to make sure it only runs in the melange context. This gave me the following error:

File "lib/backend/dune", lines 5-13, characters 0-219:
 5 | (rule
 6 |  (deps (alias ../../melange))
 7 |  (action
 8 |   (with-stdout-to
 9 |    assets.ml
10 |    (progn
11 |     (echo "let js = {|")
12 |     (run esbuild --bundle --minify --format=esm ../../output/lib/frontend/frontend.js)
13 |     (echo "|}\n")))))
Error: No rule found for alias melange

So now apparently I lost my ability to specify the melange compilation step as a dependency of the bundling rule. But without this dependency the backend will be compiled before the Javascript code (and hence the Assets module) is generated.

Is there another way to tell dune what I want? Or am I too early in the melange development process for this kind of stuff (full-stack application in one package + resource bundling at compile time)?

After reading RFC: single-context Universal Libraries · Issue #10630 · ocaml/dune · GitHub it seems like the difficulties I’m having are known issues.

I re-read the article by Antonio Monteiro and decided to go with the “copy_files hack” described there. It means I need to define my shared library under a different name (I chose shared_melange) to be able to pass the melange-specific configuration. That is not very elegant but it seems to work for now.

I updated my repo, for those who are interested.