···11+#!/usr/bin/env python3
22+"""HTTP server with CORS headers for cross-origin demo testing."""
33+import sys
44+from http.server import HTTPServer, SimpleHTTPRequestHandler
55+66+class CORSHandler(SimpleHTTPRequestHandler):
77+ def end_headers(self):
88+ self.send_header("Access-Control-Allow-Origin", "*")
99+ self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
1010+ self.send_header("Access-Control-Allow-Headers", "*")
1111+ super().end_headers()
1212+1313+ def do_OPTIONS(self):
1414+ self.send_response(200)
1515+ self.end_headers()
1616+1717+ def log_message(self, format, *args):
1818+ # Suppress request logging to keep test output clean
1919+ pass
2020+2121+if __name__ == "__main__":
2222+ port = int(sys.argv[1]) if len(sys.argv) > 1 else 9090
2323+ directory = sys.argv[2] if len(sys.argv) > 2 else "."
2424+ import os
2525+ os.chdir(directory)
2626+ server = HTTPServer(("", port), CORSHandler)
2727+ print(f"CORS server on http://localhost:{port} serving {directory}")
2828+ server.serve_forever()
+231
deploy.sh
···11+#!/bin/bash
22+# Deploy interactive OCaml demo pages.
33+#
44+# This script builds 6 demo pages that showcase in-browser OCaml evaluation
55+# using the odoc-interactive-extension and js_top_worker (jtw).
66+#
77+# ─── One-time setup ──────────────────────────────────────────────────────
88+#
99+# This script uses `jtw opam` to build universes from local opam switches.
1010+# In production, `day10 batch --with-jtw` does this at scale inside
1111+# containers (see docs/jtw-admin-guide.md). The manual switch setup
1212+# below is the local-dev path for running these demos.
1313+#
1414+# 1. Create the opam switches. The "default" switch is used for building
1515+# the monorepo and for the yojson 3.x universes.
1616+#
1717+# # default switch (should already exist)
1818+# opam switch create default ocaml-base-compiler.5.4.1
1919+# eval $(opam env --switch default --set-switch)
2020+# opam install yojson
2121+#
2222+# # yojson 2.x switch (for demo2_v2)
2323+# opam switch create demo-yojson-v2 ocaml-base-compiler.5.4.1
2424+# eval $(opam env --switch demo-yojson-v2 --set-switch)
2525+# opam install yojson.2.2.2
2626+#
2727+# # OxCaml switch (for demo3_oxcaml)
2828+# opam switch create 5.2.0+ox \
2929+# --repos ox=git+https://github.com/oxcaml/opam-repository.git,default
3030+#
3131+# 2. Pin and install js_top_worker packages in every switch that needs a
3232+# universe. From the monorepo root:
3333+#
3434+# for sw in default demo-yojson-v2 5.2.0+ox; do
3535+# eval $(opam env --switch $sw --set-switch)
3636+# opam pin add js_top_worker . --no-action
3737+# opam pin add js_top_worker-web . --no-action
3838+# opam install js_top_worker js_top_worker-web
3939+# done
4040+#
4141+# 3. Build and install the monorepo tools (jtw, odoc, the extension)
4242+# into the default switch:
4343+#
4444+# eval $(opam env --switch default --set-switch)
4545+# dune build @install && dune install
4646+#
4747+# After this, `jtw opam --help` should work.
4848+#
4949+# ─── Production alternative (day10) ─────────────────────────────────────
5050+#
5151+# For building universes at scale (e.g. all of opam), use day10 instead
5252+# of manual switches. day10 runs builds in OCI containers with cached
5353+# overlay layers. See day10/docs/ADMIN_GUIDE.md for full details.
5454+#
5555+# dune exec -- day10 batch \
5656+# --cache-dir /var/cache/day10 \
5757+# --opam-repository /var/cache/opam-repository \
5858+# --local-repo /path/to/js_top_worker \
5959+# --with-jtw \
6060+# --jtw-output /var/www/jtw \
6161+# --html-output /var/www/docs \
6262+# --with-doc \
6363+# @packages.json
6464+#
6565+# --local-repo pins js_top_worker packages from a local checkout instead
6666+# of the default remote git repo. day10 discovers *.opam files in that
6767+# directory, bind-mounts it into the container, and uses it for pinning.
6868+# This is the easiest way to test local changes to js_top_worker.
6969+#
7070+# day10 handles switch creation, dependency solving, js_top_worker
7171+# installation, and per-package universe assembly automatically.
7272+#
7373+# ─── Usage ───────────────────────────────────────────────────────────────
7474+#
7575+# ./deploy.sh # build everything and serve on port 8080
7676+# ./deploy.sh --no-serve # build only, don't start HTTP server
7777+7878+set -euo pipefail
7979+8080+MONO=$(cd "$(dirname "$0")/.." && pwd)
8181+DOC_HTML="$MONO/_build/default/_doc/_html/odoc-interactive-extension"
8282+ODOCL="$MONO/_build/default/_doc/_odocl/odoc-interactive-extension"
8383+OPAM_ODOC="$HOME/.opam/default/bin/odoc"
8484+UNIVERSES=$(mktemp -d)
8585+SERVE=true
8686+8787+if [[ "${1:-}" == "--no-serve" ]]; then
8888+ SERVE=false
8989+fi
9090+9191+echo "=== Step 1: Install odoc extension into opam switch ==="
9292+cd "$MONO"
9393+export OPAMSWITCH=default
9494+eval "$(opam env)"
9595+dune build @install
9696+dune install 2>&1 | tail -5
9797+9898+echo ""
9999+echo "=== Step 2: Build odoc docs (generates .odocl + base HTML) ==="
100100+# @doc may exit non-zero due to warnings in other packages (e.g. odoc's own
101101+# cheatsheet referencing cmdliner). We tolerate that as long as the odocl
102102+# files we need were actually produced.
103103+dune build @doc 2>&1 | tail -5 || true
104104+if [ ! -f "$ODOCL/page-demo1.odocl" ]; then
105105+ echo "ERROR: odocl files not generated — dune build @doc failed." >&2
106106+ exit 1
107107+fi
108108+109109+echo ""
110110+echo "=== Step 3: Regenerate demo HTML with opam odoc (has extension) ==="
111111+# dune's workspace-local odoc can't find dune-site plugins, so we use the
112112+# opam-installed one which has the extension registered.
113113+for page in demo1 demo2_v2 demo2_v3 demo3_oxcaml demo4_crossorigin demo5_multiverse; do
114114+ chmod u+w "$DOC_HTML/${page}.html" 2>/dev/null || true
115115+ "$OPAM_ODOC" html-generate "$ODOCL/page-${page}.odocl" \
116116+ -o "$DOC_HTML/.." \
117117+ --support-uri=_odoc_support 2>&1
118118+ echo " regenerated ${page}.html"
119119+done
120120+121121+echo ""
122122+echo "=== Step 4: Build x-ocaml.js ==="
123123+dune build x-ocaml/src/x_ocaml.bc.js
124124+125125+echo ""
126126+echo "=== Step 5: Build universes ==="
127127+128128+# 5a. Default universe (yojson 3.0 — used by demo1)
129129+echo " building default universe (yojson, default switch)..."
130130+jtw opam --switch=default -o "$UNIVERSES/default" yojson
131131+132132+# 5b. Yojson v2 universe
133133+echo " building yojson-v2 universe (demo-yojson-v2 switch)..."
134134+jtw opam --switch=demo-yojson-v2 -o "$UNIVERSES/v2" yojson
135135+136136+# 5c. Yojson v3 universe (same as default, but separate dir for isolation)
137137+echo " building yojson-v3 universe (default switch)..."
138138+jtw opam --switch=default -o "$UNIVERSES/v3" yojson
139139+140140+# 5d. OxCaml universe (stdlib only)
141141+echo " building oxcaml universe (5.2.0+ox switch)..."
142142+jtw opam --switch=5.2.0+ox -o "$UNIVERSES/oxcaml"
143143+144144+echo ""
145145+echo "=== Step 6: Deploy assets into doc HTML output ==="
146146+147147+# _x-ocaml runtime (shared by all pages)
148148+mkdir -p "$DOC_HTML/_x-ocaml"
149149+chmod -R u+w "$DOC_HTML/_x-ocaml" 2>/dev/null || true
150150+cp "$MONO/_build/default/x-ocaml/src/x_ocaml.bc.js" "$DOC_HTML/_x-ocaml/x-ocaml.js"
151151+cp "$UNIVERSES/default/worker.js" "$DOC_HTML/_x-ocaml/worker.js"
152152+echo " deployed _x-ocaml/"
153153+154154+# Make deployed dirs writable so re-runs can overwrite them.
155155+chmod -R u+w "$DOC_HTML/universe" "$DOC_HTML/universe-v2" \
156156+ "$DOC_HTML/universe-v3" "$DOC_HTML/universe-oxcaml" 2>/dev/null || true
157157+158158+# demo1: ./universe
159159+rm -rf "$DOC_HTML/universe"
160160+cp -r "$UNIVERSES/default" "$DOC_HTML/universe"
161161+echo " deployed universe/ (demo1)"
162162+163163+# demo2_v2: ./universe-v2
164164+rm -rf "$DOC_HTML/universe-v2"
165165+cp -r "$UNIVERSES/v2" "$DOC_HTML/universe-v2"
166166+echo " deployed universe-v2/ (demo2_v2)"
167167+168168+# demo2_v3: ./universe-v3
169169+rm -rf "$DOC_HTML/universe-v3"
170170+cp -r "$UNIVERSES/v3" "$DOC_HTML/universe-v3"
171171+echo " deployed universe-v3/ (demo2_v3)"
172172+173173+# demo3_oxcaml: ./universe-oxcaml
174174+rm -rf "$DOC_HTML/universe-oxcaml"
175175+cp -r "$UNIVERSES/oxcaml" "$DOC_HTML/universe-oxcaml"
176176+echo " deployed universe-oxcaml/ (demo3_oxcaml)"
177177+178178+# Copy x-ocaml.js into each universe (for cross-origin blob: fallback)
179179+for d in universe universe-v2 universe-v3 universe-oxcaml; do
180180+ cp "$DOC_HTML/_x-ocaml/x-ocaml.js" "$DOC_HTML/$d/x-ocaml.js"
181181+done
182182+183183+# Cross-origin universe (same content as default, served on port 9090)
184184+CROSSORIGIN_DIR="$DOC_HTML/../_crossorigin_universes"
185185+chmod -R u+w "$CROSSORIGIN_DIR" 2>/dev/null || true
186186+rm -rf "$CROSSORIGIN_DIR"
187187+mkdir -p "$CROSSORIGIN_DIR"
188188+cp -r "$DOC_HTML/universe" "$CROSSORIGIN_DIR/universe"
189189+echo " deployed _crossorigin_universes/ (for port 9090)"
190190+191191+# Multiverse (per-package layout with universe linking)
192192+echo ""
193193+echo "=== Step 7: Build multiverse (per-package layout) ==="
194194+MULTIVERSE_DIR="$DOC_HTML/../_multiverse"
195195+chmod -R u+w "$MULTIVERSE_DIR" 2>/dev/null || true
196196+rm -rf "$MULTIVERSE_DIR"
197197+jtw opam-all --switch=default yojson -o "$MULTIVERSE_DIR"
198198+cp "$MONO/_build/default/x-ocaml/src/x_ocaml.bc.js" "$MULTIVERSE_DIR/x-ocaml.js"
199199+echo " deployed _multiverse/ (for port 9090)"
200200+201201+echo ""
202202+echo "=== Done ==="
203203+echo "Demo pages at: $DOC_HTML/"
204204+echo ""
205205+echo " demo1.html — basic OCaml + yojson (default switch)"
206206+echo " demo2_v2.html — yojson 2.2.2 (demo-yojson-v2 switch)"
207207+echo " demo2_v3.html — yojson 3.0.0 (default switch)"
208208+echo " demo3_oxcaml.html — OxCaml extensions (5.2.0+ox switch)"
209209+echo " demo4_crossorigin.html — cross-origin loading (needs port 9090)"
210210+echo " demo5_multiverse.html — multiverse per-package layout (needs port 9090)"
211211+echo ""
212212+echo "Cross-origin demos (demo4, demo5) require a CORS-enabled server on port 9090."
213213+echo "They serve different directories, so you can only test one at a time:"
214214+echo ""
215215+echo " demo4 (cross-origin):"
216216+echo " python3 $MONO/odoc-interactive-extension/cors_server.py 9090 $DOC_HTML/../_crossorigin_universes"
217217+echo ""
218218+echo " demo5 (multiverse):"
219219+echo " python3 $MONO/odoc-interactive-extension/cors_server.py 9090 $DOC_HTML/../_multiverse"
220220+221221+# Clean up
222222+rm -rf "$UNIVERSES"
223223+224224+if $SERVE; then
225225+ echo ""
226226+ echo "Starting HTTP server on http://localhost:8080"
227227+ echo "Visit: http://localhost:8080/odoc-interactive-extension/demo1.html"
228228+ echo ""
229229+ cd "$DOC_HTML/.."
230230+ exec python3 -m http.server 8080
231231+fi
+39
doc/demo1.mld
···11+{0 Interactive OCaml Demo}
22+33+@x-ocaml.universe ./universe
44+@x-ocaml.worker ./universe/worker.js
55+66+This page demonstrates interactive OCaml code cells powered by
77+[x-ocaml] and [js_top_worker].
88+99+{1 Basic Expressions}
1010+1111+Try evaluating some OCaml expressions:
1212+1313+{@ocaml[
1414+1 + 2 * 3
1515+]}
1616+1717+{@ocaml[
1818+let greet name = Printf.sprintf "Hello, %s!" name
1919+2020+let () = print_endline (greet "World")
2121+]}
2222+2323+{1 Using Yojson}
2424+2525+These cells use the [yojson] library loaded from the universe:
2626+2727+{@ocaml[
2828+#require "yojson"
2929+]}
3030+3131+{@ocaml[
3232+let json = `Assoc [
3333+ ("name", `String "OCaml");
3434+ ("version", `Float 5.4);
3535+ ("features", `List [`String "modules"; `String "types"])
3636+]
3737+3838+let () = print_endline (Yojson.Safe.pretty_to_string json)
3939+]}
+24
doc/demo2_v2.mld
···11+{0 Yojson v2 Demo}
22+33+@x-ocaml.universe ./universe-v2
44+@x-ocaml.worker ./universe-v2/worker.js
55+66+This page uses {b yojson 2.2.2} from a separate universe directory.
77+88+{@ocaml[
99+#require "yojson"
1010+]}
1111+1212+{@ocaml[
1313+(* Yojson 2.x API *)
1414+let json = `Assoc [("key", `String "value")]
1515+let s = Yojson.Safe.to_string json
1616+let () = print_endline s
1717+]}
1818+1919+{@ocaml[
2020+(* Yojson 2.x: Yojson.Safe.prettify is a string->string function *)
2121+let ugly = Yojson.Safe.to_string (`Assoc [("compact", `Bool true); ("data", `List [`Int 1; `Int 2; `Int 3])])
2222+let pretty = Yojson.Safe.prettify ugly
2323+let () = print_endline pretty
2424+]}
+24
doc/demo2_v3.mld
···11+{0 Yojson v3 Demo}
22+33+@x-ocaml.universe ./universe-v3
44+@x-ocaml.worker ./universe-v3/worker.js
55+66+This page uses {b yojson 3.0.0} from a separate universe directory.
77+88+{@ocaml[
99+#require "yojson"
1010+]}
1111+1212+{@ocaml[
1313+(* Yojson 3.0 API *)
1414+let json = `Assoc [("key", `String "value")]
1515+let s = Yojson.Safe.to_string json
1616+let () = print_endline s
1717+]}
1818+1919+{@ocaml[
2020+(* Build and query JSON *)
2121+let parsed = `Assoc [("x", `Int 42); ("y", `String "hello")]
2222+let x = Yojson.Safe.Util.member "x" parsed
2323+let () = Printf.printf "x = %s\n" (Yojson.Safe.to_string x)
2424+]}
+84
doc/demo3_oxcaml.mld
···11+{0 OxCaml Interactive Demo}
22+33+@x-ocaml.universe ./universe-oxcaml
44+@x-ocaml.worker ./universe-oxcaml/worker.js
55+66+This page demonstrates OxCaml language extensions running interactively
77+in the browser via [x-ocaml] and [js_top_worker].
88+99+{1 List Comprehensions}
1010+1111+OxCaml adds Python/Haskell-style list and array comprehensions:
1212+1313+{@ocaml[
1414+let squares = [ x * x for x = 1 to 10 ]
1515+1616+let () = List.iter (fun x -> Printf.printf "%d " x) squares
1717+]}
1818+1919+{@ocaml[
2020+let evens = [ x for x = 1 to 20 when x mod 2 = 0 ]
2121+2222+let () = Printf.printf "Evens: %s\n"
2323+ (String.concat ", " (List.map string_of_int evens))
2424+]}
2525+2626+Nested comprehensions produce the cartesian product:
2727+2828+{@ocaml[
2929+let pairs = [ (x, y) for x = 1 to 3 for y = 1 to 3 when x <> y ]
3030+3131+let () = List.iter (fun (x, y) -> Printf.printf "(%d,%d) " x y) pairs
3232+]}
3333+3434+{1 Array Comprehensions}
3535+3636+Array comprehensions create arrays using the same syntax as list comprehensions:
3737+3838+{@ocaml[
3939+let squares = [| x * x for x = 1 to 10 |]
4040+4141+let () = Array.iter (fun x -> Printf.printf "%d " x) squares
4242+]}
4343+4444+{@ocaml[
4545+let fibs =
4646+ let a = Array.make 10 0 in
4747+ a.(0) <- 1; a.(1) <- 1;
4848+ for i = 2 to 9 do a.(i) <- a.(i-1) + a.(i-2) done;
4949+ [| a.(i) for i = 0 to 9 |]
5050+5151+let () = Array.iter (fun x -> Printf.printf "%d " x) fibs
5252+]}
5353+5454+{1 Let Mutable}
5555+5656+[let mutable] provides mutable local variables without heap allocation:
5757+5858+{@ocaml[
5959+let triangle n =
6060+ let mutable total = 0 in
6161+ for i = 1 to n do
6262+ total <- total + i
6363+ done;
6464+ total
6565+6666+let () = Printf.printf "triangle 10 = %d\n" (triangle 10)
6767+]}
6868+6969+{@ocaml[
7070+let fizzbuzz n =
7171+ let mutable result = [] in
7272+ for i = n downto 1 do
7373+ let s = match i mod 3, i mod 5 with
7474+ | 0, 0 -> "FizzBuzz"
7575+ | 0, _ -> "Fizz"
7676+ | _, 0 -> "Buzz"
7777+ | _ -> string_of_int i
7878+ in
7979+ result <- s :: result
8080+ done;
8181+ result
8282+8383+let () = print_endline (String.concat " " (fizzbuzz 15))
8484+]}
+36
doc/demo4_crossorigin.mld
···11+{0 Cross-Origin Demo}
22+33+@x-ocaml.universe http://localhost:9090/universe
44+@x-ocaml.worker http://localhost:9090/universe/worker.js
55+66+This page demonstrates {b cross-origin} loading of OCaml universes.
77+The page is served from [localhost:8080] while the worker and libraries
88+are loaded from [localhost:9090], exercising the blob: URL worker
99+creation and sync XHR + eval library loading code paths.
1010+1111+{1 Basic Expression}
1212+1313+{@ocaml[
1414+1 + 2 * 3
1515+]}
1616+1717+{@ocaml[
1818+let greet name = Printf.sprintf "Hello, %s!" name
1919+2020+let () = print_endline (greet "Cross-Origin World")
2121+]}
2222+2323+{1 Loading a Library}
2424+2525+{@ocaml[
2626+#require "yojson"
2727+]}
2828+2929+{@ocaml[
3030+let json = `Assoc [
3131+ ("origin", `String "cross-origin");
3232+ ("port", `Int 9090)
3333+]
3434+3535+let () = print_endline (Yojson.Safe.pretty_to_string json)
3636+]}
+46
doc/demo5_multiverse.mld
···11+{0 Multiverse Demo}
22+33+@x-ocaml.universe http://localhost:9090/yojson
44+@x-ocaml.worker http://localhost:9090/worker.js
55+66+This page demonstrates a {b multiverse} layout where each package is
77+built and hosted independently. The universe URL points at
88+[localhost:9090/yojson], which contains only yojson's own artifacts.
99+Stdlib is discovered automatically via the ["universes"] link in
1010+yojson's [findlib_index.json]:
1111+1212+{v
1313+yojson/findlib_index.json:
1414+ {"meta_files": ["lib/yojson/META"], "universes": ["../stdlib"]}
1515+v}
1616+1717+This is how a large-scale host like [ocaml.org] would serve packages:
1818+each package is a small self-contained directory, with links to its
1919+dependencies.
2020+2121+{1 Basic Expression}
2222+2323+{@ocaml[
2424+1 + 2 * 3
2525+]}
2626+2727+{@ocaml[
2828+let greet name = Printf.sprintf "Hello, %s!" name
2929+3030+let () = print_endline (greet "Multiverse World")
3131+]}
3232+3333+{1 Loading a Library}
3434+3535+{@ocaml[
3636+#require "yojson"
3737+]}
3838+3939+{@ocaml[
4040+let json = `Assoc [
4141+ ("source", `String "multiverse");
4242+ ("linked_universes", `Int 2)
4343+]
4444+4545+let () = print_endline (Yojson.Safe.pretty_to_string json)
4646+]}