My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

odoc-interactive-extension: add demo pages, deploy script, and test harnesses

Add 6 interactive OCaml demo .mld pages (demo1 through demo5_multiverse)
with deploy.sh to build universes, regenerate HTML with the opam-installed
odoc, and serve locally. Includes CORS server for cross-origin demos,
Playwright test scripts for cross-origin and multiverse demos, and a
worker URL meta tag in the extension.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+704
+28
odoc-interactive-extension/cors_server.py
··· 1 + #!/usr/bin/env python3 2 + """HTTP server with CORS headers for cross-origin demo testing.""" 3 + import sys 4 + from http.server import HTTPServer, SimpleHTTPRequestHandler 5 + 6 + class CORSHandler(SimpleHTTPRequestHandler): 7 + def end_headers(self): 8 + self.send_header("Access-Control-Allow-Origin", "*") 9 + self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") 10 + self.send_header("Access-Control-Allow-Headers", "*") 11 + super().end_headers() 12 + 13 + def do_OPTIONS(self): 14 + self.send_response(200) 15 + self.end_headers() 16 + 17 + def log_message(self, format, *args): 18 + # Suppress request logging to keep test output clean 19 + pass 20 + 21 + if __name__ == "__main__": 22 + port = int(sys.argv[1]) if len(sys.argv) > 1 else 9090 23 + directory = sys.argv[2] if len(sys.argv) > 2 else "." 24 + import os 25 + os.chdir(directory) 26 + server = HTTPServer(("", port), CORSHandler) 27 + print(f"CORS server on http://localhost:{port} serving {directory}") 28 + server.serve_forever()
+167
odoc-interactive-extension/deploy.sh
··· 1 + #!/bin/bash 2 + # Deploy interactive OCaml demo pages. 3 + # 4 + # Prerequisites: 5 + # - opam switches: default (OCaml 5.4, yojson 3.0), demo-yojson-v2 (yojson 2.2), 5.2.0+ox 6 + # - js_top_worker packages pinned and installed in each switch that needs a universe 7 + # - jtw (odoc-notebook) installed in default switch 8 + # - odoc-interactive-extension installed in default switch (dune install) 9 + # 10 + # Usage: 11 + # ./deploy.sh # build everything and serve on port 8080 12 + # ./deploy.sh --no-serve # build only, don't start HTTP server 13 + 14 + set -euo pipefail 15 + 16 + MONO=$(cd "$(dirname "$0")/.." && pwd) 17 + DOC_HTML="$MONO/_build/default/_doc/_html/odoc-interactive-extension" 18 + ODOCL="$MONO/_build/default/_doc/_odocl/odoc-interactive-extension" 19 + OPAM_ODOC="$HOME/.opam/default/bin/odoc" 20 + UNIVERSES=$(mktemp -d) 21 + SERVE=true 22 + 23 + if [[ "${1:-}" == "--no-serve" ]]; then 24 + SERVE=false 25 + fi 26 + 27 + echo "=== Step 1: Install odoc extension into opam switch ===" 28 + cd "$MONO" 29 + export OPAMSWITCH=default 30 + eval "$(opam env)" 31 + dune build @install 32 + dune install 2>&1 | tail -5 33 + 34 + echo "" 35 + echo "=== Step 2: Build odoc docs (generates .odocl + base HTML) ===" 36 + # @doc may exit non-zero due to warnings in other packages (e.g. odoc's own 37 + # cheatsheet referencing cmdliner). We tolerate that as long as the odocl 38 + # files we need were actually produced. 39 + dune build @doc 2>&1 | tail -5 || true 40 + if [ ! -f "$ODOCL/page-demo1.odocl" ]; then 41 + echo "ERROR: odocl files not generated — dune build @doc failed." >&2 42 + exit 1 43 + fi 44 + 45 + echo "" 46 + echo "=== Step 3: Regenerate demo HTML with opam odoc (has extension) ===" 47 + # dune's workspace-local odoc can't find dune-site plugins, so we use the 48 + # opam-installed one which has the extension registered. 49 + for page in demo1 demo2_v2 demo2_v3 demo3_oxcaml demo4_crossorigin demo5_multiverse; do 50 + chmod u+w "$DOC_HTML/${page}.html" 2>/dev/null || true 51 + "$OPAM_ODOC" html-generate "$ODOCL/page-${page}.odocl" \ 52 + -o "$DOC_HTML/.." \ 53 + --support-uri=_odoc_support 2>&1 54 + echo " regenerated ${page}.html" 55 + done 56 + 57 + echo "" 58 + echo "=== Step 4: Build x-ocaml.js ===" 59 + dune build x-ocaml/src/x_ocaml.bc.js 60 + 61 + echo "" 62 + echo "=== Step 5: Build universes ===" 63 + 64 + # 5a. Default universe (yojson 3.0 — used by demo1) 65 + echo " building default universe (yojson, default switch)..." 66 + jtw opam --switch=default -o "$UNIVERSES/default" yojson 67 + 68 + # 5b. Yojson v2 universe 69 + echo " building yojson-v2 universe (demo-yojson-v2 switch)..." 70 + jtw opam --switch=demo-yojson-v2 -o "$UNIVERSES/v2" yojson 71 + 72 + # 5c. Yojson v3 universe (same as default, but separate dir for isolation) 73 + echo " building yojson-v3 universe (default switch)..." 74 + jtw opam --switch=default -o "$UNIVERSES/v3" yojson 75 + 76 + # 5d. OxCaml universe (stdlib only) 77 + echo " building oxcaml universe (5.2.0+ox switch)..." 78 + jtw opam --switch=5.2.0+ox -o "$UNIVERSES/oxcaml" 79 + 80 + echo "" 81 + echo "=== Step 6: Deploy assets into doc HTML output ===" 82 + 83 + # _x-ocaml runtime (shared by all pages) 84 + mkdir -p "$DOC_HTML/_x-ocaml" 85 + chmod -R u+w "$DOC_HTML/_x-ocaml" 2>/dev/null || true 86 + cp "$MONO/_build/default/x-ocaml/src/x_ocaml.bc.js" "$DOC_HTML/_x-ocaml/x-ocaml.js" 87 + cp "$UNIVERSES/default/worker.js" "$DOC_HTML/_x-ocaml/worker.js" 88 + echo " deployed _x-ocaml/" 89 + 90 + # Make deployed dirs writable so re-runs can overwrite them. 91 + chmod -R u+w "$DOC_HTML/universe" "$DOC_HTML/universe-v2" \ 92 + "$DOC_HTML/universe-v3" "$DOC_HTML/universe-oxcaml" 2>/dev/null || true 93 + 94 + # demo1: ./universe 95 + rm -rf "$DOC_HTML/universe" 96 + cp -r "$UNIVERSES/default" "$DOC_HTML/universe" 97 + echo " deployed universe/ (demo1)" 98 + 99 + # demo2_v2: ./universe-v2 100 + rm -rf "$DOC_HTML/universe-v2" 101 + cp -r "$UNIVERSES/v2" "$DOC_HTML/universe-v2" 102 + echo " deployed universe-v2/ (demo2_v2)" 103 + 104 + # demo2_v3: ./universe-v3 105 + rm -rf "$DOC_HTML/universe-v3" 106 + cp -r "$UNIVERSES/v3" "$DOC_HTML/universe-v3" 107 + echo " deployed universe-v3/ (demo2_v3)" 108 + 109 + # demo3_oxcaml: ./universe-oxcaml 110 + rm -rf "$DOC_HTML/universe-oxcaml" 111 + cp -r "$UNIVERSES/oxcaml" "$DOC_HTML/universe-oxcaml" 112 + echo " deployed universe-oxcaml/ (demo3_oxcaml)" 113 + 114 + # Copy x-ocaml.js into each universe (for cross-origin blob: fallback) 115 + for d in universe universe-v2 universe-v3 universe-oxcaml; do 116 + cp "$DOC_HTML/_x-ocaml/x-ocaml.js" "$DOC_HTML/$d/x-ocaml.js" 117 + done 118 + 119 + # Cross-origin universe (same content as default, served on port 9090) 120 + CROSSORIGIN_DIR="$DOC_HTML/../_crossorigin_universes" 121 + chmod -R u+w "$CROSSORIGIN_DIR" 2>/dev/null || true 122 + rm -rf "$CROSSORIGIN_DIR" 123 + mkdir -p "$CROSSORIGIN_DIR" 124 + cp -r "$DOC_HTML/universe" "$CROSSORIGIN_DIR/universe" 125 + echo " deployed _crossorigin_universes/ (for port 9090)" 126 + 127 + # Multiverse (per-package layout with universe linking) 128 + echo "" 129 + echo "=== Step 7: Build multiverse (per-package layout) ===" 130 + MULTIVERSE_DIR="$DOC_HTML/../_multiverse" 131 + chmod -R u+w "$MULTIVERSE_DIR" 2>/dev/null || true 132 + rm -rf "$MULTIVERSE_DIR" 133 + jtw opam-all --switch=default yojson -o "$MULTIVERSE_DIR" 134 + cp "$MONO/_build/default/x-ocaml/src/x_ocaml.bc.js" "$MULTIVERSE_DIR/x-ocaml.js" 135 + echo " deployed _multiverse/ (for port 9090)" 136 + 137 + echo "" 138 + echo "=== Done ===" 139 + echo "Demo pages at: $DOC_HTML/" 140 + echo "" 141 + echo " demo1.html — basic OCaml + yojson (default switch)" 142 + echo " demo2_v2.html — yojson 2.2.2 (demo-yojson-v2 switch)" 143 + echo " demo2_v3.html — yojson 3.0.0 (default switch)" 144 + echo " demo3_oxcaml.html — OxCaml extensions (5.2.0+ox switch)" 145 + echo " demo4_crossorigin.html — cross-origin loading (needs port 9090)" 146 + echo " demo5_multiverse.html — multiverse per-package layout (needs port 9090)" 147 + echo "" 148 + echo "Cross-origin demos (demo4, demo5) require a CORS-enabled server on port 9090." 149 + echo "They serve different directories, so you can only test one at a time:" 150 + echo "" 151 + echo " demo4 (cross-origin):" 152 + echo " python3 $MONO/odoc-interactive-extension/cors_server.py 9090 $DOC_HTML/../_crossorigin_universes" 153 + echo "" 154 + echo " demo5 (multiverse):" 155 + echo " python3 $MONO/odoc-interactive-extension/cors_server.py 9090 $DOC_HTML/../_multiverse" 156 + 157 + # Clean up 158 + rm -rf "$UNIVERSES" 159 + 160 + if $SERVE; then 161 + echo "" 162 + echo "Starting HTTP server on http://localhost:8080" 163 + echo "Visit: http://localhost:8080/odoc-interactive-extension/demo1.html" 164 + echo "" 165 + cd "$DOC_HTML/.." 166 + exec python3 -m http.server 8080 167 + fi
+39
odoc-interactive-extension/doc/demo1.mld
··· 1 + {0 Interactive OCaml Demo} 2 + 3 + @x-ocaml.universe ./universe 4 + @x-ocaml.worker ./universe/worker.js 5 + 6 + This page demonstrates interactive OCaml code cells powered by 7 + [x-ocaml] and [js_top_worker]. 8 + 9 + {1 Basic Expressions} 10 + 11 + Try evaluating some OCaml expressions: 12 + 13 + {@ocaml[ 14 + 1 + 2 * 3 15 + ]} 16 + 17 + {@ocaml[ 18 + let greet name = Printf.sprintf "Hello, %s!" name 19 + 20 + let () = print_endline (greet "World") 21 + ]} 22 + 23 + {1 Using Yojson} 24 + 25 + These cells use the [yojson] library loaded from the universe: 26 + 27 + {@ocaml[ 28 + #require "yojson" 29 + ]} 30 + 31 + {@ocaml[ 32 + let json = `Assoc [ 33 + ("name", `String "OCaml"); 34 + ("version", `Float 5.4); 35 + ("features", `List [`String "modules"; `String "types"]) 36 + ] 37 + 38 + let () = print_endline (Yojson.Safe.pretty_to_string json) 39 + ]}
+24
odoc-interactive-extension/doc/demo2_v2.mld
··· 1 + {0 Yojson v2 Demo} 2 + 3 + @x-ocaml.universe ./universe-v2 4 + @x-ocaml.worker ./universe-v2/worker.js 5 + 6 + This page uses {b yojson 2.2.2} from a separate universe directory. 7 + 8 + {@ocaml[ 9 + #require "yojson" 10 + ]} 11 + 12 + {@ocaml[ 13 + (* Yojson 2.x API *) 14 + let json = `Assoc [("key", `String "value")] 15 + let s = Yojson.Safe.to_string json 16 + let () = print_endline s 17 + ]} 18 + 19 + {@ocaml[ 20 + (* Yojson 2.x: Yojson.Safe.prettify is a string->string function *) 21 + let ugly = Yojson.Safe.to_string (`Assoc [("compact", `Bool true); ("data", `List [`Int 1; `Int 2; `Int 3])]) 22 + let pretty = Yojson.Safe.prettify ugly 23 + let () = print_endline pretty 24 + ]}
+24
odoc-interactive-extension/doc/demo2_v3.mld
··· 1 + {0 Yojson v3 Demo} 2 + 3 + @x-ocaml.universe ./universe-v3 4 + @x-ocaml.worker ./universe-v3/worker.js 5 + 6 + This page uses {b yojson 3.0.0} from a separate universe directory. 7 + 8 + {@ocaml[ 9 + #require "yojson" 10 + ]} 11 + 12 + {@ocaml[ 13 + (* Yojson 3.0 API *) 14 + let json = `Assoc [("key", `String "value")] 15 + let s = Yojson.Safe.to_string json 16 + let () = print_endline s 17 + ]} 18 + 19 + {@ocaml[ 20 + (* Build and query JSON *) 21 + let parsed = `Assoc [("x", `Int 42); ("y", `String "hello")] 22 + let x = Yojson.Safe.Util.member "x" parsed 23 + let () = Printf.printf "x = %s\n" (Yojson.Safe.to_string x) 24 + ]}
+84
odoc-interactive-extension/doc/demo3_oxcaml.mld
··· 1 + {0 OxCaml Interactive Demo} 2 + 3 + @x-ocaml.universe ./universe-oxcaml 4 + @x-ocaml.worker ./universe-oxcaml/worker.js 5 + 6 + This page demonstrates OxCaml language extensions running interactively 7 + in the browser via [x-ocaml] and [js_top_worker]. 8 + 9 + {1 List Comprehensions} 10 + 11 + OxCaml adds Python/Haskell-style list and array comprehensions: 12 + 13 + {@ocaml[ 14 + let squares = [ x * x for x = 1 to 10 ] 15 + 16 + let () = List.iter (fun x -> Printf.printf "%d " x) squares 17 + ]} 18 + 19 + {@ocaml[ 20 + let evens = [ x for x = 1 to 20 when x mod 2 = 0 ] 21 + 22 + let () = Printf.printf "Evens: %s\n" 23 + (String.concat ", " (List.map string_of_int evens)) 24 + ]} 25 + 26 + Nested comprehensions produce the cartesian product: 27 + 28 + {@ocaml[ 29 + let pairs = [ (x, y) for x = 1 to 3 for y = 1 to 3 when x <> y ] 30 + 31 + let () = List.iter (fun (x, y) -> Printf.printf "(%d,%d) " x y) pairs 32 + ]} 33 + 34 + {1 Array Comprehensions} 35 + 36 + Array comprehensions create arrays using the same syntax as list comprehensions: 37 + 38 + {@ocaml[ 39 + let squares = [| x * x for x = 1 to 10 |] 40 + 41 + let () = Array.iter (fun x -> Printf.printf "%d " x) squares 42 + ]} 43 + 44 + {@ocaml[ 45 + let fibs = 46 + let a = Array.make 10 0 in 47 + a.(0) <- 1; a.(1) <- 1; 48 + for i = 2 to 9 do a.(i) <- a.(i-1) + a.(i-2) done; 49 + [| a.(i) for i = 0 to 9 |] 50 + 51 + let () = Array.iter (fun x -> Printf.printf "%d " x) fibs 52 + ]} 53 + 54 + {1 Let Mutable} 55 + 56 + [let mutable] provides mutable local variables without heap allocation: 57 + 58 + {@ocaml[ 59 + let triangle n = 60 + let mutable total = 0 in 61 + for i = 1 to n do 62 + total <- total + i 63 + done; 64 + total 65 + 66 + let () = Printf.printf "triangle 10 = %d\n" (triangle 10) 67 + ]} 68 + 69 + {@ocaml[ 70 + let fizzbuzz n = 71 + let mutable result = [] in 72 + for i = n downto 1 do 73 + let s = match i mod 3, i mod 5 with 74 + | 0, 0 -> "FizzBuzz" 75 + | 0, _ -> "Fizz" 76 + | _, 0 -> "Buzz" 77 + | _ -> string_of_int i 78 + in 79 + result <- s :: result 80 + done; 81 + result 82 + 83 + let () = print_endline (String.concat " " (fizzbuzz 15)) 84 + ]}
+36
odoc-interactive-extension/doc/demo4_crossorigin.mld
··· 1 + {0 Cross-Origin Demo} 2 + 3 + @x-ocaml.universe http://localhost:9090/universe 4 + @x-ocaml.worker http://localhost:9090/universe/worker.js 5 + 6 + This page demonstrates {b cross-origin} loading of OCaml universes. 7 + The page is served from [localhost:8080] while the worker and libraries 8 + are loaded from [localhost:9090], exercising the blob: URL worker 9 + creation and sync XHR + eval library loading code paths. 10 + 11 + {1 Basic Expression} 12 + 13 + {@ocaml[ 14 + 1 + 2 * 3 15 + ]} 16 + 17 + {@ocaml[ 18 + let greet name = Printf.sprintf "Hello, %s!" name 19 + 20 + let () = print_endline (greet "Cross-Origin World") 21 + ]} 22 + 23 + {1 Loading a Library} 24 + 25 + {@ocaml[ 26 + #require "yojson" 27 + ]} 28 + 29 + {@ocaml[ 30 + let json = `Assoc [ 31 + ("origin", `String "cross-origin"); 32 + ("port", `Int 9090) 33 + ] 34 + 35 + let () = print_endline (Yojson.Safe.pretty_to_string json) 36 + ]}
+46
odoc-interactive-extension/doc/demo5_multiverse.mld
··· 1 + {0 Multiverse Demo} 2 + 3 + @x-ocaml.universe http://localhost:9090/yojson 4 + @x-ocaml.worker http://localhost:9090/worker.js 5 + 6 + This page demonstrates a {b multiverse} layout where each package is 7 + built and hosted independently. The universe URL points at 8 + [localhost:9090/yojson], which contains only yojson's own artifacts. 9 + Stdlib is discovered automatically via the ["universes"] link in 10 + yojson's [findlib_index.json]: 11 + 12 + {v 13 + yojson/findlib_index.json: 14 + {"meta_files": ["lib/yojson/META"], "universes": ["../stdlib"]} 15 + v} 16 + 17 + This is how a large-scale host like [ocaml.org] would serve packages: 18 + each package is a small self-contained directory, with links to its 19 + dependencies. 20 + 21 + {1 Basic Expression} 22 + 23 + {@ocaml[ 24 + 1 + 2 * 3 25 + ]} 26 + 27 + {@ocaml[ 28 + let greet name = Printf.sprintf "Hello, %s!" name 29 + 30 + let () = print_endline (greet "Multiverse World") 31 + ]} 32 + 33 + {1 Loading a Library} 34 + 35 + {@ocaml[ 36 + #require "yojson" 37 + ]} 38 + 39 + {@ocaml[ 40 + let json = `Assoc [ 41 + ("source", `String "multiverse"); 42 + ("linked_universes", `Int 2) 43 + ] 44 + 45 + let () = print_endline (Yojson.Safe.pretty_to_string json) 46 + ]}
+2
odoc-interactive-extension/doc/dune
··· 1 + (documentation 2 + (package odoc-interactive-extension))
+4
odoc-interactive-extension/src/interactive_extension.ml
··· 19 19 code block resources. *) 20 20 21 21 let universe_url = ref None 22 + let worker_url = ref None 22 23 let requires : string list ref = ref [] 23 24 24 25 (** {1 HTML helpers} *) ··· 67 68 | "universe" -> 68 69 universe_url := Some text; 69 70 [ Api.Js_inline (meta_tag_script "x-ocaml-universe" text) ] 71 + | "worker" -> 72 + worker_url := Some text; 73 + [ Api.Js_inline (meta_tag_script "x-ocaml-worker" text) ] 70 74 | "requires" -> 71 75 let pkgs = 72 76 List.filter (fun s -> s <> "")
+82
odoc-interactive-extension/test_crossorigin.js
··· 1 + // test_crossorigin.js — Playwright test for cross-origin demo 2 + const { chromium } = require('playwright'); 3 + 4 + (async () => { 5 + const browser = await chromium.launch(); 6 + const page = await browser.newPage(); 7 + 8 + const consoleMessages = []; 9 + page.on('console', msg => consoleMessages.push(msg.text())); 10 + 11 + const errors = []; 12 + page.on('pageerror', err => errors.push(err.message)); 13 + 14 + console.log('Navigating to demo4_crossorigin.html...'); 15 + await page.goto('http://localhost:8080/odoc-interactive-extension/demo4_crossorigin.html'); 16 + 17 + // Wait for cells to render and execute (worker init + eval) 18 + console.log('Waiting for cells to execute...'); 19 + await page.waitForTimeout(15000); 20 + 21 + // Check for cross-origin indicators in console 22 + const hasCrossOriginWorker = consoleMessages.some(m => 23 + m.includes('9090')); 24 + const hasCrossOriginFetch = consoleMessages.some(m => 25 + m.includes('Cross-origin import via fetch+eval')); 26 + const hasInitFinished = consoleMessages.some(m => 27 + m.includes('init() finished')); 28 + const hasSetupFinished = consoleMessages.some(m => 29 + m.includes('setup() finished')); 30 + 31 + // Check for init_error 32 + const hasInitError = consoleMessages.some(m => 33 + m.includes('init_error')); 34 + 35 + // Check for correct output in page (x-ocaml uses Shadow DOM, so we 36 + // look at worker output messages in the console instead) 37 + const hasInt7 = consoleMessages.some(m => 38 + m.includes('int = 7')); 39 + const hasGreeting = consoleMessages.some(m => 40 + m.includes('Hello, Cross-Origin World')); 41 + 42 + console.log(''); 43 + console.log('=== Results ==='); 44 + console.log(` Worker loaded from port 9090: ${hasCrossOriginWorker ? 'YES' : 'NO'}`); 45 + console.log(` Cross-origin fetch+eval used: ${hasCrossOriginFetch ? 'YES' : 'NO'}`); 46 + console.log(` Init completed: ${hasInitFinished ? 'YES' : 'NO'}`); 47 + console.log(` Setup completed: ${hasSetupFinished ? 'YES' : 'NO'}`); 48 + console.log(` Init error: ${hasInitError ? 'YES (BAD)' : 'NO (good)'}`); 49 + console.log(` Output "int = 7": ${hasInt7 ? 'YES' : 'NO'}`); 50 + console.log(` Output "Hello, Cross-Origin": ${hasGreeting ? 'YES' : 'NO'}`); 51 + 52 + let passed = true; 53 + 54 + if (!hasInitFinished) { 55 + console.log('\nFAIL: Worker init did not complete'); 56 + passed = false; 57 + } 58 + if (!hasSetupFinished) { 59 + console.log('\nFAIL: Worker setup did not complete'); 60 + passed = false; 61 + } 62 + if (hasInitError) { 63 + console.log('\nFAIL: Worker reported init_error'); 64 + const errMsg = consoleMessages.find(m => m.includes('init_error')); 65 + console.log(` ${errMsg}`); 66 + passed = false; 67 + } 68 + if (!hasInt7 || !hasGreeting) { 69 + console.log('\nFAIL: Expected output not found in page'); 70 + passed = false; 71 + } 72 + 73 + if (passed) { 74 + console.log('\nPASS: Cross-origin demo working correctly'); 75 + } else { 76 + console.log('\n--- Console messages (last 40) ---'); 77 + consoleMessages.slice(-40).forEach(m => console.log(` ${m.substring(0, 150)}`)); 78 + } 79 + 80 + await browser.close(); 81 + process.exit(passed ? 0 : 1); 82 + })();
+54
odoc-interactive-extension/test_crossorigin.sh
··· 1 + #!/bin/bash 2 + # Test cross-origin demo end-to-end. 3 + # Starts page server (8080) and CORS universe server (9090), 4 + # then runs Playwright to verify cross-origin loading works. 5 + set -euo pipefail 6 + 7 + SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) 8 + MONO=$(cd "$SCRIPT_DIR/.." && pwd) 9 + DOC_HTML="$MONO/_build/default/_doc/_html/odoc-interactive-extension" 10 + CROSSORIGIN_DIR="$MONO/_build/default/_doc/_html/_crossorigin_universes" 11 + 12 + cleanup() { 13 + [[ -n "${PAGE_PID:-}" ]] && kill "$PAGE_PID" 2>/dev/null || true 14 + [[ -n "${CORS_PID:-}" ]] && kill "$CORS_PID" 2>/dev/null || true 15 + wait 2>/dev/null || true 16 + } 17 + trap cleanup EXIT 18 + 19 + echo "=== Building demos ===" 20 + bash "$SCRIPT_DIR/deploy.sh" --no-serve 21 + 22 + echo "" 23 + echo "=== Starting servers ===" 24 + 25 + # Page server on port 8080 26 + cd "$DOC_HTML/.." 27 + python3 -m http.server 8080 &>/dev/null & 28 + PAGE_PID=$! 29 + 30 + # CORS universe server on port 9090 31 + python3 "$SCRIPT_DIR/cors_server.py" 9090 "$CROSSORIGIN_DIR" &>/dev/null & 32 + CORS_PID=$! 33 + 34 + sleep 2 35 + echo " Page server: http://localhost:8080 (PID $PAGE_PID)" 36 + echo " CORS server: http://localhost:9090 (PID $CORS_PID)" 37 + 38 + # Verify both servers respond 39 + curl -sf http://localhost:8080/odoc-interactive-extension/demo4_crossorigin.html > /dev/null \ 40 + || { echo "FAIL: page server not responding"; exit 1; } 41 + curl -sf http://localhost:9090/universe/findlib_index.json > /dev/null \ 42 + || { echo "FAIL: CORS server not responding"; exit 1; } 43 + 44 + # Verify CORS headers 45 + CORS_HEADER=$(curl -sI http://localhost:9090/universe/findlib_index.json | grep -i 'access-control-allow-origin' || true) 46 + if [[ -z "$CORS_HEADER" ]]; then 47 + echo "FAIL: CORS server missing Access-Control-Allow-Origin header" 48 + exit 1 49 + fi 50 + echo " CORS header: $CORS_HEADER" 51 + 52 + echo "" 53 + echo "=== Running Playwright test ===" 54 + NODE_PATH="$MONO/x-ocaml/test/node_modules" node "$SCRIPT_DIR/test_crossorigin.js"
+114
odoc-interactive-extension/test_multiverse.js
··· 1 + // test_multiverse.js — Playwright test for multiverse demo (per-package layout) 2 + const { chromium } = require('playwright'); 3 + 4 + (async () => { 5 + const browser = await chromium.launch(); 6 + const page = await browser.newPage(); 7 + 8 + const consoleMessages = []; 9 + page.on('console', msg => consoleMessages.push(msg.text())); 10 + 11 + const errors = []; 12 + page.on('pageerror', err => errors.push(err.message)); 13 + 14 + console.log('Navigating to demo5_multiverse.html...'); 15 + await page.goto('http://localhost:8080/odoc-interactive-extension/demo5_multiverse.html'); 16 + 17 + // Wait for cells to render and execute (worker init + universe linking + eval) 18 + console.log('Waiting for cells to execute...'); 19 + await page.waitForTimeout(15000); 20 + 21 + // Check multiverse-specific behavior in console 22 + const hasWorkerFrom9090 = consoleMessages.some(m => 23 + m.includes('9090')); 24 + const hasStdlibFallback = consoleMessages.some(m => 25 + m.includes('stdlib_dcs not found') && m.includes('findlib discovery')); 26 + const hasStdlibDiscovered = consoleMessages.some(m => 27 + m.includes('Found stdlib dcs via findlib')); 28 + const hasDcsRewrite = consoleMessages.some(m => 29 + m.includes('Rewriting dcs_url')); 30 + const hasUniverseLinking = consoleMessages.some(m => 31 + m.includes('universes')); 32 + const hasInitFinished = consoleMessages.some(m => 33 + m.includes('init() finished')); 34 + const hasSetupFinished = consoleMessages.some(m => 35 + m.includes('setup() finished')); 36 + const hasInitError = consoleMessages.some(m => 37 + m.includes('init_error')); 38 + 39 + // Check for correct outputs 40 + const hasInt7 = consoleMessages.some(m => 41 + m.includes('int = 7')); 42 + const hasGreeting = consoleMessages.some(m => 43 + m.includes('Hello, Multiverse World')); 44 + const hasYojsonLoaded = consoleMessages.some(m => 45 + m.includes('Custom #require: yojson loaded')); 46 + const hasJsonOutput = consoleMessages.some(m => 47 + m.includes('"source": "multiverse"') || m.includes('multiverse')); 48 + 49 + console.log(''); 50 + console.log('=== Results ==='); 51 + console.log(` Worker loaded from port 9090: ${hasWorkerFrom9090 ? 'YES' : 'NO'}`); 52 + console.log(` stdlib_dcs fallback triggered: ${hasStdlibFallback ? 'YES' : 'NO'}`); 53 + console.log(` Stdlib discovered via findlib: ${hasStdlibDiscovered ? 'YES' : 'NO'}`); 54 + console.log(` dcs_url rewritten to absolute: ${hasDcsRewrite ? 'YES' : 'NO'}`); 55 + console.log(` Universe linking used: ${hasUniverseLinking ? 'YES' : 'NO'}`); 56 + console.log(` Init completed: ${hasInitFinished ? 'YES' : 'NO'}`); 57 + console.log(` Setup completed: ${hasSetupFinished ? 'YES' : 'NO'}`); 58 + console.log(` Init error: ${hasInitError ? 'YES (BAD)' : 'NO (good)'}`); 59 + console.log(` Output "int = 7": ${hasInt7 ? 'YES' : 'NO'}`); 60 + console.log(` Output "Hello, Multiverse": ${hasGreeting ? 'YES' : 'NO'}`); 61 + console.log(` Yojson loaded via #require: ${hasYojsonLoaded ? 'YES' : 'NO'}`); 62 + console.log(` JSON output with "multiverse": ${hasJsonOutput ? 'YES' : 'NO'}`); 63 + 64 + let passed = true; 65 + 66 + if (!hasStdlibFallback) { 67 + console.log('\nFAIL: stdlib_dcs fallback was not triggered (multiverse needs it)'); 68 + passed = false; 69 + } 70 + if (!hasStdlibDiscovered) { 71 + console.log('\nFAIL: Stdlib was not discovered via findlib universe linking'); 72 + passed = false; 73 + } 74 + if (!hasDcsRewrite) { 75 + console.log('\nFAIL: dcs_url was not rewritten to absolute URL'); 76 + passed = false; 77 + } 78 + if (!hasInitFinished) { 79 + console.log('\nFAIL: Worker init did not complete'); 80 + passed = false; 81 + } 82 + if (!hasSetupFinished) { 83 + console.log('\nFAIL: Worker setup did not complete'); 84 + passed = false; 85 + } 86 + if (hasInitError) { 87 + console.log('\nFAIL: Worker reported init_error'); 88 + const errMsg = consoleMessages.find(m => m.includes('init_error')); 89 + console.log(` ${errMsg}`); 90 + passed = false; 91 + } 92 + if (!hasInt7) { 93 + console.log('\nFAIL: Expected output "int = 7" not found'); 94 + passed = false; 95 + } 96 + if (!hasGreeting) { 97 + console.log('\nFAIL: Expected output "Hello, Multiverse World" not found'); 98 + passed = false; 99 + } 100 + if (!hasYojsonLoaded) { 101 + console.log('\nFAIL: Yojson was not loaded via #require'); 102 + passed = false; 103 + } 104 + 105 + if (passed) { 106 + console.log('\nPASS: Multiverse demo working correctly'); 107 + } else { 108 + console.log('\n--- Console messages (last 50) ---'); 109 + consoleMessages.slice(-50).forEach(m => console.log(` ${m.substring(0, 200)}`)); 110 + } 111 + 112 + await browser.close(); 113 + process.exit(passed ? 0 : 1); 114 + })();