···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
odoc-interactive-extension/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
odoc-interactive-extension/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
odoc-interactive-extension/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
odoc-interactive-extension/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
odoc-interactive-extension/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-]}
···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-]}