My aggregated monorepo of OCaml code, automaintained
at main 301 lines 9.9 kB view raw view rendered
1# Interactive OCaml Demos — End-to-End Setup 2 3This document explains how to build and serve the interactive OCaml demo 4pages that use the `odoc-interactive-extension` and `js_top_worker` (jtw) 5to create executable code cells inside odoc documentation. 6 7## Overview 8 9The system has three main components: 10 111. **odoc-interactive-extension** — an odoc plugin that transforms 12 `{@ocaml[...]}` code blocks into `<x-ocaml>` custom elements and 13 processes `@x-ocaml.universe` tags. 14 152. **x-ocaml** — a WebComponent (`<x-ocaml>`) that creates CodeMirror 16 editors, connects to a Web Worker for OCaml evaluation, and renders 17 output inline. 18 193. **jtw** (js_top_worker) — builds "universe" directories containing 20 compiled OCaml libraries (`.cma.js` files, `.cmi` stubs, findlib 21 metadata) that the worker loads at runtime. 22 23### Demo pages 24 25| Page | Description | Universe | 26|------|-------------|----------| 27| `demo1.html` | Basic expressions + Yojson | `./universe` (yojson from default switch) | 28| `demo2_v2.html` | Yojson 2.x API | `./universe-v2` (yojson from demo-yojson-v2 switch) | 29| `demo2_v3.html` | Yojson 3.x API | `./universe-v3` (yojson from default switch) | 30 31The v2 and v3 demos show that different library versions can coexist as 32separate universes, each page loading its own set of compiled libraries. 33 34## Prerequisites 35 36- OCaml 5.4+ with opam 37- The monorepo checked out at `/home/jons-agent/workspace/mono` (or adjust paths) 38- Two opam switches: 39 - `default` — has yojson 3.x (or latest) 40 - `demo-yojson-v2` — has yojson 2.2.2 41- Python 3 (for the dev server, or any static file server with CORS) 42 43### Creating the yojson v2 switch (one-time) 44 45```bash 46opam switch create demo-yojson-v2 ocaml-base-compiler.5.4.1 47eval $(opam env --switch demo-yojson-v2 --set-switch) 48opam install yojson.2.2.2 49``` 50 51## Build Steps 52 53All commands assume you start from the monorepo root. 54 55### Step 1: Build and install the toolchain 56 57The odoc extension must be installed before generating docs, because odoc 58loads extensions as plugins at doc-generation time. 59 60```bash 61eval $(opam env --switch default --set-switch) 62dune build @install 63``` 64 65This builds and installs (locally) `odoc`, `jtw`, `x-ocaml`, and the 66`odoc-interactive-extension` plugin. 67 68### Step 2: Generate odoc HTML 69 70```bash 71dune build @doc 72``` 73 74This generates HTML for all packages. The demo `.mld` files 75(`odoc-interactive-extension/doc/demo1.mld`, etc.) are compiled as part 76of the `odoc-interactive-extension` package documentation. 77 78The HTML output lands in: 79``` 80_build/default/_doc/_html/odoc-interactive-extension/ 81├── demo1.html 82├── demo2_v2.html 83├── demo2_v3.html 84└── index.html 85``` 86 87### Step 3: Copy x-ocaml assets 88 89The extension loads `x-ocaml.js` and `worker.js` from a local 90`_x-ocaml/` directory relative to the HTML pages. Copy them from the 91dune build output: 92 93```bash 94HTMLDIR=_build/default/_doc/_html/odoc-interactive-extension 95mkdir -p "$HTMLDIR/_x-ocaml" 96cp _build/default/x-ocaml/src/x_ocaml.bc.js "$HTMLDIR/_x-ocaml/x-ocaml.js" 97cp _build/default/js_top_worker/lib/.worker.eobjs/jsoo/worker.bc.js "$HTMLDIR/_x-ocaml/worker.js" 98``` 99 100### Step 4: Build universe directories 101 102Each demo page declares a universe via `@x-ocaml.universe ./universe-name` 103in its `.mld` source. We use `jtw opam` to generate each universe 104directory. 105 106The `jtw` binary is at `_build/install/default/bin/jtw`. 107 108```bash 109JTW=_build/install/default/bin/jtw 110 111# Universe for demo1 (default switch, yojson latest) 112eval $(opam env --switch default --set-switch) 113$JTW opam --no-worker --output "$HTMLDIR/universe" yojson 114 115# Universe for demo2_v3 (default switch, yojson latest = 3.x) 116$JTW opam --no-worker --output "$HTMLDIR/universe-v3" yojson 117 118# Universe for demo2_v2 (demo-yojson-v2 switch, yojson 2.2.2) 119eval $(opam env --switch demo-yojson-v2 --set-switch) 120$JTW opam --no-worker --switch demo-yojson-v2 --output "$HTMLDIR/universe-v2" yojson 121 122# Switch back to default 123eval $(opam env --switch default --set-switch) 124``` 125 126Key flags: 127- `--no-worker` — skip building a worker.js (we provide our own via `_x-ocaml/`) 128- `--output DIR` — where to write the universe files 129- `--switch NAME` — which opam switch to read packages from 130- `yojson` — positional argument: the library to include (plus its deps) 131 132Each universe directory will contain: 133``` 134universe/ 135├── findlib_index.json # lists META file locations 136└── lib/ 137 ├── ocaml/ 138 │ ├── META 139 │ ├── dynamic_cmis.json # lists .cmi files to load on demand 140 │ └── *.cmi # stdlib type interfaces 141 ├── yojson/ 142 │ ├── META 143 │ ├── *.cmi 144 │ └── yojson.cma.js # compiled library code 145 └── seq/ # (dependency, if needed) 146 └── ... 147``` 148 149### Step 5: Serve and test 150 151Start a local HTTP server from the HTML root: 152 153```bash 154python3 docs/demos/cors_server.py 8080 _build/default/_doc/_html 155``` 156 157Then open in a browser: 158- http://localhost:8080/odoc-interactive-extension/demo1.html 159- http://localhost:8080/odoc-interactive-extension/demo2_v2.html 160- http://localhost:8080/odoc-interactive-extension/demo2_v3.html 161 162Each page should: 1631. Auto-evaluate all code cells on load 1642. Show output inline below each expression (e.g., `- : int = 7`) 1653. Display stdout output (e.g., `Hello, World!`) 166 167## Writing Demo Pages 168 169Demo pages are standard odoc `.mld` files with two additions: 170 171### Universe tag 172 173Declares the universe directory (relative to the HTML page): 174 175``` 176@x-ocaml.universe ./universe 177``` 178 179This makes the extension use the `jtw` backend (js_top_worker) instead of 180the builtin merlin-js backend. The worker loads libraries from the 181specified universe directory. 182 183### Code cells 184 185Standard odoc `{@ocaml[...]}` blocks become interactive cells: 186 187``` 188{@ocaml[ 1891 + 2 * 3 190]} 191``` 192 193For cells that use `#require` or reference libraries: 194 195``` 196{@ocaml[ 197#require "yojson" 198]} 199 200{@ocaml[ 201let json = `Assoc [("key", `String "value")] 202let () = print_endline (Yojson.Safe.to_string json) 203]} 204``` 205 206Note: The `#require` directive must be in a separate cell before the code 207that uses the library, because each cell is evaluated independently and 208`#require` loads the library's `.cma.js` file. 209 210### Cell modes (extension syntax) 211 212For the extended `{x@ocaml ... x[...]x}` syntax, additional attributes 213control cell behaviour: 214 215``` 216{x@ocaml hidden x[let helper = 42]x} (* invisible setup cell *) 217{x@ocaml exercise id=solve x[...]x} (* editable cell *) 218{x@ocaml test for=solve x[assert (...)]x} (* test cell, linked *) 219{x@ocaml interactive x[read_only_code]x} (* default: visible, read-only *) 220``` 221 222## Architecture Notes 223 224### Same-origin constraint 225 226Web Workers cannot be loaded from a different origin. The extension 227therefore always loads `x-ocaml.js` and `worker.js` from the local 228`_x-ocaml/` directory (same origin as the page). 229 230Universe directories can technically be cross-origin for fetching 231`findlib_index.json` and `.cmi` files (these use `fetch()`), but 232`.cma.js` files are loaded via `importScripts()` inside the worker, 233which also requires same-origin in modern browsers. For simplicity, 234keep universe directories same-origin (relative paths in the 235`@x-ocaml.universe` tag). 236 237### How the pieces connect 238 239``` 240Page Load 241242 ├─ odoc extension emits: 243 │ <meta name="x-ocaml-universe" content="./universe"> 244 │ <script src="./_x-ocaml/x-ocaml.js" src-worker="./_x-ocaml/worker.js" backend="jtw"> 245 │ <x-ocaml mode="interactive">code...</x-ocaml> 246247 ├─ x-ocaml.js (WebComponent): 248 │ 1. Reads <meta> tags for universe URL 249 │ 2. Creates Web Worker from worker.js 250 │ 3. Sends init message with findlib_index URL 251 │ 4. For each <x-ocaml> cell: 252 │ - Creates CodeMirror editor 253 │ - Sends eval message to worker 254 │ - Renders streaming output as decorations 255256 └─ worker.js (js_top_worker): 257 1. Receives init with findlib_index URL 258 2. Fetches findlib_index.json → learns library locations 259 3. Fetches dynamic_cmis.json → learns available .cmi files 260 4. On eval: compiles OCaml code using toplevel 261 5. On #require: loads .cma.js via importScripts 262 6. Sends output_at (per-phrase) and output (final) messages 263``` 264 265### jtw_client.ml — the bridge 266 267`x-ocaml/src/jtw_client.ml` bridges x-ocaml's `X_protocol` with 268js_top_worker's message protocol. Key design decisions: 269 270- Uses `Jtw.eval_stream` (not `Jtw.eval`) to capture per-phrase 271 `output_at` messages, which contain the actual evaluation results 272 (e.g., `val x : int = 3`). The `Jtw.eval` function only returns the 273 final `output` message, which often has an empty `caml_ppf`. 274 275- Derives `stdlib_dcs` (the URL for `dynamic_cmis.json`) from the 276 `findlib_index` URL, so the worker can locate stdlib CMIs regardless 277 of whether the universe is at a relative or absolute path. 278 279## Troubleshooting 280 281**No output after clicking Run / cells don't auto-evaluate** 282- Check browser console for worker errors 283- Verify `_x-ocaml/worker.js` exists and is accessible 284- Verify the universe's `findlib_index.json` is accessible 285 286**"Toplevel not initialized" error** 287- The worker.js must be built with `js_of_ocaml-toplevel` and the flags 288 `--toplevel +toplevel.js +dynlink.js` 289 290**"Unbound module Yojson" after #require** 291- Check that `#require "yojson"` is in a separate earlier cell 292- Verify the universe directory contains `lib/yojson/yojson.cma.js` 293 294**Cross-origin errors** 295- Keep universe directories same-origin (use relative paths in 296 `@x-ocaml.universe`) 297- The `_x-ocaml/` directory must be on the same origin as the HTML page 298 299**Console shows 404 for .cmi files** 300- Normal if the code doesn't reference those modules; the worker 301 fetches CMIs on demand and some 404s are expected for unused modules