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: consolidate dual-compiler builds, per-package findlib, cross-jsoo compat

- Consolidate js_top_worker and odoc dual-compiler stanzas into single
library stanzas with cppo rules generating impl.ml from impl.cppo.ml
- Per-package findlib_index.json with relative universe paths (../dep)
and implicit stdlib dependency injection
- Add find_stdlib_dcs to Impl.S interface for stdlib CMI lookup via
findlib metadata instead of hardcoded URLs
- Replace jsoo Json.output/Json.unsafe_input with plain JSON.stringify/
JSON.parse for cross-jsoo-version compatibility (6.0.1+ox vs 6.2.0)
- Cross-origin worker support: set __global_rel_url in blob worker,
skip URL rewriting for absolute http(s) URLs
- Fix odoc doc comments and ocamlformat-ignore for cppo files
- Add demo docs, helper scripts, and x-ocaml package-lock.json

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

+1956 -141
+1
cmd
··· 1 + _build/install/default/bin/jtw opam --switch 5.2.0+ox -o /tmp/oxcaml angstrom angstrom.async angstrom.lwt-unix angstrom.unix astring astring.top base64 base64.rfc2045 basement bigstringaf bos bos.setup bos.top brr brr.ocaml_poke brr.ocaml_poke_ui brr.poke brr.poked bytes camlp-streams capsule0 capsule0.blocking_sync capsule0.expert chrome-trace cmdliner compiler-libs compiler-libs.bytecomp compiler-libs.common compiler-libs.optcomp compiler-libs.toplevel cppo crunch csexp cstruct domain-local-await dune dune-action-plugin dune-build-info dune-configurator dune-glob dune-private-libs dune-private-libs.dune-section dune-private-libs.meta_parser dune-rpc dune-rpc.private dune-site dune-site.dynlink dune-site.linker dune-site.plugins dune-site.private dune-site.toplevel dune.configurator dyn dynlink eio eio.core eio.mock eio.runtime_events eio.unix eio.utils eio_linux eio_main eio_posix either findlib findlib.dynload findlib.internal findlib.top fix fmt fmt.cli fmt.top fmt.tty fpath fpath.top fs-io gen hmap iomux jane-street-headers js_of_ocaml js_of_ocaml-compiler js_of_ocaml-compiler.dynlink js_of_ocaml-compiler.findlib-support js_of_ocaml-compiler.runtime js_of_ocaml-compiler.runtime-files js_of_ocaml-lwt js_of_ocaml-ppx js_of_ocaml-ppx.as-lib js_of_ocaml-toplevel js_of_ocaml.deriving js_top_worker js_top_worker-bin js_top_worker-rpc js_top_worker-rpc.message js_top_worker-web jsonm logs logs.browser logs.cli logs.fmt logs.lwt logs.threaded logs.top lwt lwt-dllist lwt.unix mdx mdx.__private__ mdx.__private__.odoc_parser mdx.test mdx.top menhir menhirCST menhirGLR menhirLib menhirSdk merlin-lib merlin-lib.analysis merlin-lib.commands merlin-lib.config merlin-lib.dot_protocol merlin-lib.extend merlin-lib.index_format merlin-lib.kernel merlin-lib.ocaml_merlin_specific merlin-lib.ocaml_parsing merlin-lib.ocaml_preprocess merlin-lib.ocaml_typing merlin-lib.ocaml_utils merlin-lib.os_ipc merlin-lib.query_commands merlin-lib.query_protocol merlin-lib.sherlodoc merlin-lib.utils mime_printer mtime mtime.clock mtime.clock.os mtime.top ocaml-compiler-libs ocaml-compiler-libs.bytecomp ocaml-compiler-libs.common ocaml-compiler-libs.optcomp ocaml-compiler-libs.shadow ocaml-compiler-libs.toplevel ocaml-syntax-shims ocaml-version ocaml_intrinsics_kernel ocamlbuild ocamlc-loc ocamlgraph ocp-indent ocp-indent.dynlink ocp-indent.lexer ocp-indent.lib ocp-indent.utils ocplib-endian ocplib-endian.bigstring odoc-parser opam-core opam-core.cmdliner opam-file-format opam-format optint ordering patch pp ppx_array_base ppx_base ppx_blob ppx_cold ppx_compare ppx_compare.expander ppx_compare.runtime-lib ppx_derivers ppx_deriving ppx_deriving.api ppx_deriving.create ppx_deriving.enum ppx_deriving.eq ppx_deriving.fold ppx_deriving.iter ppx_deriving.make ppx_deriving.map ppx_deriving.ord ppx_deriving.runtime ppx_deriving.show ppx_deriving.std ppx_enumerate ppx_enumerate.runtime-lib ppx_globalize ppx_hash ppx_hash.base_internalhash_types ppx_hash.expander ppx_hash.runtime-lib ppx_helpers ppx_helpers.modes_lib ppx_js_style ppx_sexp_conv ppx_sexp_conv.expander ppx_sexp_conv.runtime-lib ppx_shorthand ppx_template ppx_template.expander ppxlib ppxlib.ast ppxlib.astlib ppxlib.metaquot ppxlib.metaquot_lifters ppxlib.print_diff ppxlib.runner ppxlib.runner_as_ppx ppxlib.stdppx ppxlib.traverse ppxlib.traverse_builtins ppxlib_ast ppxlib_ast.ast ppxlib_ast.astlib ppxlib_ast.stdppx ppxlib_ast.traverse_builtins ppxlib_jane psq ptime ptime.clock ptime.clock.os ptime.top re re.emacs re.glob re.pcre re.perl re.posix re.str result rpclib rpclib-lwt rpclib.cmdliner rpclib.core rpclib.internals rpclib.json rpclib.markdown rpclib.xml rresult rresult.top runtime_events sedlex sedlex.ppx sedlex.utils seq sexp_type sexp_type.grammar sexplib0 sha stdlib stdlib-shims stdlib_alpha stdlib_beta stdlib_stable stdlib_upstream_compatible stdune str stringext swhid_core thread-table threads threads.posix top-closure topkg tyxml tyxml.functor unix uri uri.services uri.services_full uring uucp uuseg uuseg.string uutf xdg xmlm yojson zarith_stubs_js
-8
day10/day10.install
··· 1 - lib: [ 2 - "_build/install/default/lib/day10/META" 3 - "_build/install/default/lib/day10/dune-package" 4 - "_build/install/default/lib/day10/opam" 5 - ] 6 - bin: [ 7 - "_build/install/default/bin/day10" 8 - ]
+301
docs/demos/INTERACTIVE_OCAML_DEMOS.md
··· 1 + # Interactive OCaml Demos — End-to-End Setup 2 + 3 + This document explains how to build and serve the interactive OCaml demo 4 + pages that use the `odoc-interactive-extension` and `js_top_worker` (jtw) 5 + to create executable code cells inside odoc documentation. 6 + 7 + ## Overview 8 + 9 + The system has three main components: 10 + 11 + 1. **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 + 15 + 2. **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 + 19 + 3. **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 + 31 + The v2 and v3 demos show that different library versions can coexist as 32 + separate 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 46 + opam switch create demo-yojson-v2 ocaml-base-compiler.5.4.1 47 + eval $(opam env --switch demo-yojson-v2 --set-switch) 48 + opam install yojson.2.2.2 49 + ``` 50 + 51 + ## Build Steps 52 + 53 + All commands assume you start from the monorepo root. 54 + 55 + ### Step 1: Build and install the toolchain 56 + 57 + The odoc extension must be installed before generating docs, because odoc 58 + loads extensions as plugins at doc-generation time. 59 + 60 + ```bash 61 + eval $(opam env --switch default --set-switch) 62 + dune build @install 63 + ``` 64 + 65 + This 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 71 + dune build @doc 72 + ``` 73 + 74 + This generates HTML for all packages. The demo `.mld` files 75 + (`odoc-interactive-extension/doc/demo1.mld`, etc.) are compiled as part 76 + of the `odoc-interactive-extension` package documentation. 77 + 78 + The 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 + 89 + The extension loads `x-ocaml.js` and `worker.js` from a local 90 + `_x-ocaml/` directory relative to the HTML pages. Copy them from the 91 + dune build output: 92 + 93 + ```bash 94 + HTMLDIR=_build/default/_doc/_html/odoc-interactive-extension 95 + mkdir -p "$HTMLDIR/_x-ocaml" 96 + cp _build/default/x-ocaml/src/x_ocaml.bc.js "$HTMLDIR/_x-ocaml/x-ocaml.js" 97 + cp _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 + 102 + Each demo page declares a universe via `@x-ocaml.universe ./universe-name` 103 + in its `.mld` source. We use `jtw opam` to generate each universe 104 + directory. 105 + 106 + The `jtw` binary is at `_build/install/default/bin/jtw`. 107 + 108 + ```bash 109 + JTW=_build/install/default/bin/jtw 110 + 111 + # Universe for demo1 (default switch, yojson latest) 112 + eval $(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) 119 + eval $(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 123 + eval $(opam env --switch default --set-switch) 124 + ``` 125 + 126 + Key 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 + 132 + Each universe directory will contain: 133 + ``` 134 + universe/ 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 + 151 + Start a local HTTP server from the HTML root: 152 + 153 + ```bash 154 + python3 docs/demos/cors_server.py 8080 _build/default/_doc/_html 155 + ``` 156 + 157 + Then 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 + 162 + Each page should: 163 + 1. Auto-evaluate all code cells on load 164 + 2. Show output inline below each expression (e.g., `- : int = 7`) 165 + 3. Display stdout output (e.g., `Hello, World!`) 166 + 167 + ## Writing Demo Pages 168 + 169 + Demo pages are standard odoc `.mld` files with two additions: 170 + 171 + ### Universe tag 172 + 173 + Declares the universe directory (relative to the HTML page): 174 + 175 + ``` 176 + @x-ocaml.universe ./universe 177 + ``` 178 + 179 + This makes the extension use the `jtw` backend (js_top_worker) instead of 180 + the builtin merlin-js backend. The worker loads libraries from the 181 + specified universe directory. 182 + 183 + ### Code cells 184 + 185 + Standard odoc `{@ocaml[...]}` blocks become interactive cells: 186 + 187 + ``` 188 + {@ocaml[ 189 + 1 + 2 * 3 190 + ]} 191 + ``` 192 + 193 + For cells that use `#require` or reference libraries: 194 + 195 + ``` 196 + {@ocaml[ 197 + #require "yojson" 198 + ]} 199 + 200 + {@ocaml[ 201 + let json = `Assoc [("key", `String "value")] 202 + let () = print_endline (Yojson.Safe.to_string json) 203 + ]} 204 + ``` 205 + 206 + Note: The `#require` directive must be in a separate cell before the code 207 + that 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 + 212 + For the extended `{x@ocaml ... x[...]x}` syntax, additional attributes 213 + control 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 + 226 + Web Workers cannot be loaded from a different origin. The extension 227 + therefore always loads `x-ocaml.js` and `worker.js` from the local 228 + `_x-ocaml/` directory (same origin as the page). 229 + 230 + Universe 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, 233 + which also requires same-origin in modern browsers. For simplicity, 234 + keep universe directories same-origin (relative paths in the 235 + `@x-ocaml.universe` tag). 236 + 237 + ### How the pieces connect 238 + 239 + ``` 240 + Page Load 241 + 242 + ├─ 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> 246 + 247 + ├─ 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 255 + 256 + └─ 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 268 + js_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
+22
docs/demos/cors_server.py
··· 1 + #!/usr/bin/env python3 2 + """Simple static file server with CORS headers.""" 3 + import http.server 4 + import sys 5 + import os 6 + 7 + class CORSHandler(http.server.SimpleHTTPRequestHandler): 8 + def end_headers(self): 9 + self.send_header("Access-Control-Allow-Origin", "*") 10 + self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") 11 + self.send_header("Access-Control-Allow-Headers", "*") 12 + super().end_headers() 13 + 14 + def do_OPTIONS(self): 15 + self.send_response(200) 16 + self.end_headers() 17 + 18 + port = int(sys.argv[1]) if len(sys.argv) > 1 else 8081 19 + directory = sys.argv[2] if len(sys.argv) > 2 else "." 20 + os.chdir(directory) 21 + print(f"Serving {directory} on port {port} with CORS") 22 + http.server.HTTPServer(("", port), CORSHandler).serve_forever()
+482
docs/plans/2026-02-21-end-to-end-demos.md
··· 1 + # End-to-End Interactive OCaml Demos 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Create two working demos: (1) a self-contained odoc tutorial with interactive OCaml cells, and (2) a cross-origin demo where two mld files use different universe versions served from a separate HTTP server. 6 + 7 + **Architecture:** Build universes with `jtw opam`, write `.mld` files using `@x-ocaml.universe` and `{@ocaml ...}` code blocks, build HTML with `dune build @doc`. Demo 1 serves everything from one origin. Demo 2 serves universe assets from a CORS-enabled static server on a different port, with two universes containing different versions of `yojson`. 8 + 9 + **Tech Stack:** `jtw` CLI, `dune build @doc`, odoc-interactive-extension, x-ocaml.js, Python HTTP servers 10 + 11 + --- 12 + 13 + ## Prerequisites 14 + 15 + All commands run from the monorepo root: `/home/jons-agent/workspace/mono` 16 + 17 + Key paths (after `dune build`): 18 + - jtw binary: `_build/default/js_top_worker/bin/jtw.exe` 19 + - x-ocaml.js: `_build/default/x-ocaml/x-ocaml.js` 20 + - HTML docs output: `_build/default/_doc/_html/` 21 + 22 + The odoc-interactive-extension is loaded automatically via dune-site — no env vars needed. 23 + 24 + --- 25 + 26 + ### Task 1: Fix backend attribute in odoc-interactive-extension 27 + 28 + The script loader in `interactive_extension.ml` hardcodes `backend='builtin'`. When using a jtw-built universe, it must be `'jtw'`. Fix it to default to `jtw` when a universe URL is set. 29 + 30 + **Files:** 31 + - Modify: `odoc-interactive-extension/src/interactive_extension.ml:136-138` 32 + 33 + **Step 1: Fix the backend selection** 34 + 35 + In `interactive_extension.ml`, change the script loader to use `jtw` backend when a universe is configured: 36 + 37 + ```ocaml 38 + let backend = match !universe_url with Some _ -> "jtw" | None -> "builtin" in 39 + let script_loader = Printf.sprintf 40 + {|(function(){if(window.__xOcamlLoaded)return;window.__xOcamlLoaded=true;var s=document.createElement('script');s.src='%s/x-ocaml.js';s.setAttribute('src-worker','%s/worker.js');s.setAttribute('backend','%s');document.head.appendChild(s)})();|} 41 + (js_escape base) (js_escape base) (js_escape backend) 42 + ``` 43 + 44 + **Step 2: Rebuild** 45 + 46 + Run: `dune build` 47 + Expected: clean build, exit 0 48 + 49 + **Step 3: Commit** 50 + 51 + ``` 52 + fix: use jtw backend when universe URL is configured 53 + ``` 54 + 55 + --- 56 + 57 + ### Task 2: Create a second opam switch with older yojson 58 + 59 + We need two incompatible yojson versions. The default switch has yojson 3.0.0. Create a switch with yojson 2.2.2 (last v2 release — different API: `Yojson.Safe.to_string` exists in both but `Yojson.Safe.read_json` only in v2, and v3 removes some legacy functions). 60 + 61 + **Step 1: Create the switch** 62 + 63 + ```bash 64 + opam switch create demo-yojson-v2 ocaml-base-compiler.5.4.1 65 + ``` 66 + 67 + Expected: switch creation completes (may take several minutes) 68 + 69 + **Step 2: Install packages in the new switch** 70 + 71 + ```bash 72 + opam install --switch=demo-yojson-v2 yojson.2.2.2 js_of_ocaml-compiler 73 + ``` 74 + 75 + We need `js_of_ocaml-compiler` because `jtw opam` compiles `.cma` to `.cma.js`. 76 + 77 + Expected: installation completes 78 + 79 + **Step 3: Verify versions** 80 + 81 + ```bash 82 + opam list --switch=demo-yojson-v2 yojson 83 + opam list --switch=default yojson 84 + ``` 85 + 86 + Expected: demo-yojson-v2 shows 2.2.2, default shows 3.0.0 87 + 88 + --- 89 + 90 + ### Task 3: Build universe A (yojson v3, default switch) 91 + 92 + **Step 1: Build the monorepo first** 93 + 94 + ```bash 95 + dune build 96 + ``` 97 + 98 + **Step 2: Build universe A** 99 + 100 + ```bash 101 + mkdir -p /tmp/demo-universes 102 + 103 + _build/default/js_top_worker/bin/jtw.exe opam yojson \ 104 + -o /tmp/demo-universes/yojson-v3 \ 105 + --copy-file _build/default/x-ocaml/x-ocaml.js 106 + ``` 107 + 108 + Expected: output directory created with `worker.js`, `x-ocaml.js`, `findlib_index.json`, and package directories. 109 + 110 + **Step 3: Verify contents** 111 + 112 + ```bash 113 + ls /tmp/demo-universes/yojson-v3/ 114 + # Should show: worker.js, x-ocaml.js, findlib_index.json, lib/ (or package dirs) 115 + ``` 116 + 117 + --- 118 + 119 + ### Task 4: Build universe B (yojson v2, demo switch) 120 + 121 + **Step 1: Build universe B** 122 + 123 + ```bash 124 + _build/default/js_top_worker/bin/jtw.exe opam yojson \ 125 + -o /tmp/demo-universes/yojson-v2 \ 126 + --switch demo-yojson-v2 \ 127 + --copy-file _build/default/x-ocaml/x-ocaml.js 128 + ``` 129 + 130 + Expected: output directory with yojson v2 compiled. 131 + 132 + **Step 2: Verify both universes exist** 133 + 134 + ```bash 135 + ls /tmp/demo-universes/yojson-v3/ 136 + ls /tmp/demo-universes/yojson-v2/ 137 + ``` 138 + 139 + --- 140 + 141 + ### Task 5: Write the Demo 1 mld file (self-contained) 142 + 143 + Create a simple `.mld` tutorial page that uses the `@x-ocaml.universe` tag and `{@ocaml ...}` code blocks. This will be built by `dune build @doc` and served from one origin. 144 + 145 + **Files:** 146 + - Create: `odoc-interactive-extension/doc/demo1.mld` 147 + - Modify: `odoc-interactive-extension/dune-project` (add doc stanza if needed) 148 + 149 + **Step 1: Create the mld file** 150 + 151 + Create `odoc-interactive-extension/doc/demo1.mld`: 152 + 153 + ``` 154 + {0 Interactive OCaml Demo} 155 + 156 + @x-ocaml.universe ./universe 157 + 158 + This page demonstrates interactive OCaml code cells powered by 159 + [x-ocaml] and [js_top_worker]. 160 + 161 + {1 Basic Expressions} 162 + 163 + Try evaluating some OCaml expressions: 164 + 165 + {@ocaml[ 166 + 1 + 2 * 3 167 + ]} 168 + 169 + {@ocaml[ 170 + let greet name = Printf.sprintf "Hello, %s!" name 171 + 172 + let () = print_endline (greet "World") 173 + ]} 174 + 175 + {1 Using Yojson} 176 + 177 + These cells use the [yojson] library loaded from the universe: 178 + 179 + {@ocaml[ 180 + #require "yojson" 181 + ]} 182 + 183 + {@ocaml[ 184 + let json = `Assoc [ 185 + ("name", `String "OCaml"); 186 + ("version", `Float 5.4); 187 + ("features", `List [`String "modules"; `String "types"]) 188 + ] 189 + 190 + let () = print_endline (Yojson.Safe.pretty_to_string json) 191 + ]} 192 + ``` 193 + 194 + **Step 2: Check that the doc stanza exists** 195 + 196 + Check `odoc-interactive-extension/dune-project` — it should already generate docs. If there is no `(documentation ...)` stanza in a `doc/dune` file, create `odoc-interactive-extension/doc/dune`: 197 + 198 + ```dune 199 + (documentation 200 + (package odoc-interactive-extension)) 201 + ``` 202 + 203 + **Step 3: Build docs** 204 + 205 + ```bash 206 + dune build @doc 207 + ``` 208 + 209 + Expected: HTML generated at `_build/default/_doc/_html/odoc-interactive-extension/demo1.html` (or similar path). 210 + 211 + **Step 4: Copy the universe into the doc output** 212 + 213 + The mld references `./universe` relative to the HTML page. We need to put the universe files alongside the generated HTML: 214 + 215 + ```bash 216 + DOC_DIR=$(find _build/default/_doc/_html -path "*/odoc-interactive-extension" -type d | head -1) 217 + cp -r /tmp/demo-universes/yojson-v3 "$DOC_DIR/universe" 218 + ``` 219 + 220 + **Step 5: Serve and test** 221 + 222 + ```bash 223 + python3 -m http.server 8080 --directory _build/default/_doc/_html/ 224 + ``` 225 + 226 + Open browser to: `http://localhost:8080/odoc-interactive-extension/demo1.html` 227 + 228 + Expected: page loads, code cells appear with Run buttons, OCaml expressions execute in the browser. 229 + 230 + **Step 6: Commit the mld file** 231 + 232 + ``` 233 + feat: add self-contained interactive OCaml demo (Demo 1) 234 + ``` 235 + 236 + --- 237 + 238 + ### Task 6: Write the Demo 2 mld files (cross-origin, two yojson versions) 239 + 240 + Two `.mld` files, each pointing at a different universe on a different server. 241 + 242 + **Files:** 243 + - Create: `odoc-interactive-extension/doc/demo2_v3.mld` 244 + - Create: `odoc-interactive-extension/doc/demo2_v2.mld` 245 + 246 + **Step 1: Create demo2_v3.mld (yojson v3)** 247 + 248 + ``` 249 + {0 Yojson v3 Demo} 250 + 251 + @x-ocaml.universe http://localhost:8081/yojson-v3 252 + 253 + This page uses {b yojson 3.0.0} served from a separate origin. 254 + 255 + {@ocaml[ 256 + #require "yojson" 257 + ]} 258 + 259 + {@ocaml[ 260 + (* Yojson 3.0 API *) 261 + let json = `Assoc [("key", `String "value")] 262 + let s = Yojson.Safe.to_string json 263 + let () = print_endline s 264 + ]} 265 + 266 + {@ocaml[ 267 + (* Parse JSON from string *) 268 + let parsed = Yojson.Safe.from_string {|{"x": 42}|} 269 + let x = Yojson.Safe.Util.member "x" parsed 270 + let () = Printf.printf "x = %s\n" (Yojson.Safe.to_string x) 271 + ]} 272 + ``` 273 + 274 + **Step 2: Create demo2_v2.mld (yojson v2)** 275 + 276 + ``` 277 + {0 Yojson v2 Demo} 278 + 279 + @x-ocaml.universe http://localhost:8081/yojson-v2 280 + 281 + This page uses {b yojson 2.2.2} served from a separate origin. 282 + 283 + {@ocaml[ 284 + #require "yojson" 285 + ]} 286 + 287 + {@ocaml[ 288 + (* Yojson 2.x API *) 289 + let json = `Assoc [("key", `String "value")] 290 + let s = Yojson.Safe.to_string json 291 + let () = print_endline s 292 + ]} 293 + 294 + {@ocaml[ 295 + (* Yojson 2.x: Yojson.Safe.prettify is a string->string function *) 296 + let ugly = {|{"compact":true,"data":[1,2,3]}|} 297 + let pretty = Yojson.Safe.prettify ugly 298 + let () = print_endline pretty 299 + ]} 300 + ``` 301 + 302 + Note: The exact API calls may need adjustment based on what actually differs between yojson 2.x and 3.x. The key point is each page loads from a different universe URL. 303 + 304 + **Step 3: Build docs** 305 + 306 + ```bash 307 + dune build @doc 308 + ``` 309 + 310 + **Step 4: Commit** 311 + 312 + ``` 313 + feat: add cross-origin yojson v2/v3 demo mld files (Demo 2) 314 + ``` 315 + 316 + --- 317 + 318 + ### Task 7: Serve Demo 2 with cross-origin setup 319 + 320 + Two servers: odoc HTML on port 8080, universe assets on port 8081 with CORS. 321 + 322 + **Step 1: Create a CORS-enabled static server script** 323 + 324 + Create `docs/demos/cors_server.py`: 325 + 326 + ```python 327 + #!/usr/bin/env python3 328 + """Simple static file server with CORS headers.""" 329 + import http.server 330 + import sys 331 + import os 332 + 333 + class CORSHandler(http.server.SimpleHTTPRequestHandler): 334 + def end_headers(self): 335 + self.send_header("Access-Control-Allow-Origin", "*") 336 + self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") 337 + self.send_header("Access-Control-Allow-Headers", "*") 338 + super().end_headers() 339 + 340 + def do_OPTIONS(self): 341 + self.send_response(200) 342 + self.end_headers() 343 + 344 + port = int(sys.argv[1]) if len(sys.argv) > 1 else 8081 345 + directory = sys.argv[2] if len(sys.argv) > 2 else "." 346 + os.chdir(directory) 347 + print(f"Serving {directory} on port {port} with CORS") 348 + http.server.HTTPServer(("", port), CORSHandler).serve_forever() 349 + ``` 350 + 351 + **Step 2: Start the universe server (port 8081)** 352 + 353 + ```bash 354 + python3 docs/demos/cors_server.py 8081 /tmp/demo-universes/ 355 + ``` 356 + 357 + This serves both `/yojson-v3/` and `/yojson-v2/` directories. 358 + 359 + **Step 3: Start the docs server (port 8080)** 360 + 361 + In a separate terminal: 362 + 363 + ```bash 364 + python3 -m http.server 8080 --directory _build/default/_doc/_html/ 365 + ``` 366 + 367 + **Step 4: Test in browser** 368 + 369 + Open two tabs: 370 + - `http://localhost:8080/odoc-interactive-extension/demo2_v3.html` 371 + - `http://localhost:8080/odoc-interactive-extension/demo2_v2.html` 372 + 373 + Expected: 374 + - Both pages load x-ocaml.js and worker.js from `http://localhost:8081/yojson-v{2,3}/` 375 + - The v3 page can `#require "yojson"` and use yojson 3.0 API 376 + - The v2 page can `#require "yojson"` and use yojson 2.x API 377 + - Each page is isolated — loading yojson in one doesn't affect the other 378 + 379 + **Step 5: Commit the CORS server script** 380 + 381 + ``` 382 + feat: add CORS static server for cross-origin universe demos 383 + ``` 384 + 385 + --- 386 + 387 + ### Task 8: End-to-end verification 388 + 389 + Run through both demos completely to verify everything works. 390 + 391 + **Step 1: Clean build** 392 + 393 + ```bash 394 + dune clean && dune build 395 + ``` 396 + 397 + **Step 2: Build both universes** 398 + 399 + ```bash 400 + rm -rf /tmp/demo-universes 401 + mkdir -p /tmp/demo-universes 402 + 403 + # Universe A: yojson v3 (default switch) 404 + _build/default/js_top_worker/bin/jtw.exe opam yojson \ 405 + -o /tmp/demo-universes/yojson-v3 \ 406 + --copy-file _build/default/x-ocaml/x-ocaml.js 407 + 408 + # Universe B: yojson v2 (demo switch) 409 + _build/default/js_top_worker/bin/jtw.exe opam yojson \ 410 + -o /tmp/demo-universes/yojson-v2 \ 411 + --switch demo-yojson-v2 \ 412 + --copy-file _build/default/x-ocaml/x-ocaml.js 413 + ``` 414 + 415 + **Step 3: Build docs** 416 + 417 + ```bash 418 + dune build @doc 419 + ``` 420 + 421 + **Step 4: Set up Demo 1 universe** 422 + 423 + ```bash 424 + DOC_DIR=$(find _build/default/_doc/_html -path "*/odoc-interactive-extension" -type d | head -1) 425 + cp -r /tmp/demo-universes/yojson-v3 "$DOC_DIR/universe" 426 + ``` 427 + 428 + **Step 5: Launch servers** 429 + 430 + Terminal 1 (docs on 8080): 431 + ```bash 432 + python3 -m http.server 8080 --directory _build/default/_doc/_html/ 433 + ``` 434 + 435 + Terminal 2 (universes on 8081 with CORS): 436 + ```bash 437 + python3 docs/demos/cors_server.py 8081 /tmp/demo-universes/ 438 + ``` 439 + 440 + **Step 6: Verify Demo 1** 441 + 442 + Open: `http://localhost:8080/odoc-interactive-extension/demo1.html` 443 + 444 + Check: 445 + - [ ] Page renders with odoc styling 446 + - [ ] Code cells appear 447 + - [ ] Clicking Run executes OCaml expressions 448 + - [ ] `#require "yojson"` loads the library 449 + - [ ] Yojson expressions produce output 450 + 451 + **Step 7: Verify Demo 2** 452 + 453 + Open: `http://localhost:8080/odoc-interactive-extension/demo2_v3.html` 454 + 455 + Check: 456 + - [ ] x-ocaml.js loads from `http://localhost:8081/yojson-v3/` 457 + - [ ] Code cells execute with yojson 3.0 API 458 + 459 + Open: `http://localhost:8080/odoc-interactive-extension/demo2_v2.html` 460 + 461 + Check: 462 + - [ ] x-ocaml.js loads from `http://localhost:8081/yojson-v2/` 463 + - [ ] Code cells execute with yojson 2.x API 464 + - [ ] The two pages use genuinely different yojson versions 465 + 466 + **Step 8: Verify cross-origin isolation** 467 + 468 + Open browser dev tools Network tab on each Demo 2 page. Confirm: 469 + - Script requests go to `localhost:8081` (different origin) 470 + - CORS headers are present on responses 471 + - No mixed-content or CORS errors in console 472 + 473 + --- 474 + 475 + ## Notes 476 + 477 + - The `jtw opam` output includes `worker.js` by default. This is the compiled OCaml toplevel (~70MB). Building it takes a few minutes. 478 + - The `--copy-file` flag copies `x-ocaml.js` into the universe directory so both script and worker are co-located. 479 + - The odoc-interactive-extension uses `dune-site` for automatic plugin loading — no manual registration needed. 480 + - The `@x-ocaml.universe` tag sets the base URL. The extension appends `/x-ocaml.js` and `/worker.js` to it. 481 + - For cross-origin, the universe server must send `Access-Control-Allow-Origin: *` headers. 482 + - Creating the second opam switch (`demo-yojson-v2`) is the slowest step — expect 5-10 minutes for the compiler build.
+389
docs/plans/2026-02-22-cross-origin-demo.md
··· 1 + # Cross-Origin Demo Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Create a cross-origin demo that exercises all three cross-origin code paths (blob: URL worker creation, sync XHR + eval for library loading, and cross-origin findlib_index fetching), plus a test script that verifies it works. 6 + 7 + **Architecture:** Pages served from port 8080, universes from a CORS-enabled server on port 9090. New `demo5_crossorigin.mld` points `@x-ocaml.universe` and `@x-ocaml.worker` at absolute `http://localhost:9090/...` URLs. A `cors_server.py` script provides the CORS headers. A `test_crossorigin.sh` script automates the full verification. 8 + 9 + **Tech Stack:** Python (CORS HTTP server), odoc mld markup, Playwright (browser testing), bash 10 + 11 + --- 12 + 13 + ### Task 1: Create the CORS-enabled HTTP server 14 + 15 + **Files:** 16 + - Create: `odoc-interactive-extension/cors_server.py` 17 + 18 + **Step 1: Write cors_server.py** 19 + 20 + This is a minimal Python HTTP server that adds `Access-Control-Allow-Origin: *` to every response. It serves a directory on a specified port. 21 + 22 + ```python 23 + #!/usr/bin/env python3 24 + """HTTP server with CORS headers for cross-origin demo testing.""" 25 + import sys 26 + from http.server import HTTPServer, SimpleHTTPRequestHandler 27 + 28 + class CORSHandler(SimpleHTTPRequestHandler): 29 + def end_headers(self): 30 + self.send_header("Access-Control-Allow-Origin", "*") 31 + self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") 32 + self.send_header("Access-Control-Allow-Headers", "*") 33 + super().end_headers() 34 + 35 + def do_OPTIONS(self): 36 + self.send_response(200) 37 + self.end_headers() 38 + 39 + def log_message(self, format, *args): 40 + # Suppress request logging to keep test output clean 41 + pass 42 + 43 + if __name__ == "__main__": 44 + port = int(sys.argv[1]) if len(sys.argv) > 1 else 9090 45 + directory = sys.argv[2] if len(sys.argv) > 2 else "." 46 + import os 47 + os.chdir(directory) 48 + server = HTTPServer(("", port), CORSHandler) 49 + print(f"CORS server on http://localhost:{port} serving {directory}") 50 + server.serve_forever() 51 + ``` 52 + 53 + **Step 2: Verify it works manually** 54 + 55 + Run: `python3 odoc-interactive-extension/cors_server.py 9090 /tmp &` 56 + Run: `curl -v http://localhost:9090/ 2>&1 | grep -i 'access-control'` 57 + Expected: `Access-Control-Allow-Origin: *` 58 + 59 + **Step 3: Commit** 60 + 61 + ```bash 62 + git add odoc-interactive-extension/cors_server.py 63 + git commit -m "feat: add CORS-enabled HTTP server for cross-origin demo" 64 + ``` 65 + 66 + --- 67 + 68 + ### Task 2: Create the cross-origin demo mld page 69 + 70 + **Files:** 71 + - Create: `odoc-interactive-extension/doc/demo5_crossorigin.mld` 72 + 73 + **Step 1: Write demo5_crossorigin.mld** 74 + 75 + This demo uses the same yojson universe as demo1, but loads it from a different origin (port 9090). The key difference is absolute URLs in the `@x-ocaml.universe` and `@x-ocaml.worker` tags. 76 + 77 + ``` 78 + {0 Cross-Origin Demo} 79 + 80 + @x-ocaml.universe http://localhost:9090/universe 81 + @x-ocaml.worker http://localhost:9090/universe/worker.js 82 + 83 + This page demonstrates {b cross-origin} loading of OCaml universes. 84 + The page is served from [localhost:8080] while the worker and libraries 85 + are loaded from [localhost:9090], exercising the blob: URL worker 86 + creation and sync XHR + eval library loading code paths. 87 + 88 + {1 Basic Expression} 89 + 90 + {@ocaml[ 91 + 1 + 2 * 3 92 + ]} 93 + 94 + {@ocaml[ 95 + let greet name = Printf.sprintf "Hello, %s!" name 96 + 97 + let () = print_endline (greet "Cross-Origin World") 98 + ]} 99 + 100 + {1 Loading a Library} 101 + 102 + {@ocaml[ 103 + #require "yojson" 104 + ]} 105 + 106 + {@ocaml[ 107 + let json = `Assoc [ 108 + ("origin", `String "cross-origin"); 109 + ("port", `Int 9090) 110 + ] 111 + 112 + let () = print_endline (Yojson.Safe.pretty_to_string json) 113 + ]} 114 + ``` 115 + 116 + **Step 2: Commit** 117 + 118 + ```bash 119 + git add odoc-interactive-extension/doc/demo5_crossorigin.mld 120 + git commit -m "feat: add cross-origin demo mld page" 121 + ``` 122 + 123 + --- 124 + 125 + ### Task 3: Update deploy.sh to include the cross-origin demo 126 + 127 + **Files:** 128 + - Modify: `odoc-interactive-extension/deploy.sh` 129 + 130 + **Step 1: Add demo5_crossorigin to the HTML regeneration loop (line 42)** 131 + 132 + Change the `for page in` loop from: 133 + ```bash 134 + for page in demo1 demo2_v2 demo2_v3 demo3_oxcaml; do 135 + ``` 136 + to: 137 + ```bash 138 + for page in demo1 demo2_v2 demo2_v3 demo3_oxcaml demo5_crossorigin; do 139 + ``` 140 + 141 + **Step 2: Add cross-origin universe deployment after the existing universe deploys (after line 105)** 142 + 143 + After the existing `for d in ... done` loop that copies x-ocaml.js, add: 144 + 145 + ```bash 146 + # Cross-origin universe (same content as default, served on port 9090) 147 + CROSSORIGIN_DIR="$DOC_HTML/../_crossorigin_universes" 148 + rm -rf "$CROSSORIGIN_DIR" 149 + mkdir -p "$CROSSORIGIN_DIR" 150 + cp -r "$DOC_HTML/universe" "$CROSSORIGIN_DIR/universe" 151 + echo " deployed _crossorigin_universes/ (for port 9090)" 152 + ``` 153 + 154 + **Step 3: Update the "Done" output to mention demo5** 155 + 156 + Add to the echo block: 157 + ```bash 158 + echo " demo5_crossorigin.html — cross-origin loading (port 9090)" 159 + echo "" 160 + echo "Cross-origin demo requires a second server:" 161 + echo " python3 odoc-interactive-extension/cors_server.py 9090 $DOC_HTML/../_crossorigin_universes" 162 + ``` 163 + 164 + **Step 4: Commit** 165 + 166 + ```bash 167 + git add odoc-interactive-extension/deploy.sh 168 + git commit -m "feat: deploy.sh supports cross-origin demo" 169 + ``` 170 + 171 + --- 172 + 173 + ### Task 4: Create the cross-origin test script 174 + 175 + **Files:** 176 + - Create: `odoc-interactive-extension/test_crossorigin.sh` 177 + 178 + **Step 1: Write test_crossorigin.sh** 179 + 180 + This script: 181 + 1. Runs `deploy.sh --no-serve` to build everything 182 + 2. Starts the page server (port 8080) and CORS universe server (port 9090) 183 + 3. Opens demo5_crossorigin.html in Playwright 184 + 4. Waits for cells to execute 185 + 5. Checks that: (a) cells produced correct output, (b) console shows cross-origin log messages 186 + 6. Exits with 0 on success, 1 on failure 187 + 188 + ```bash 189 + #!/bin/bash 190 + # Test cross-origin demo end-to-end. 191 + # Requires: npm install (for playwright) in this directory. 192 + set -euo pipefail 193 + 194 + SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) 195 + MONO=$(cd "$SCRIPT_DIR/.." && pwd) 196 + DOC_HTML="$MONO/_build/default/_doc/_html/odoc-interactive-extension" 197 + CROSSORIGIN_DIR="$MONO/_build/default/_doc/_html/_crossorigin_universes" 198 + 199 + cleanup() { 200 + [[ -n "${PAGE_PID:-}" ]] && kill "$PAGE_PID" 2>/dev/null || true 201 + [[ -n "${CORS_PID:-}" ]] && kill "$CORS_PID" 2>/dev/null || true 202 + wait 2>/dev/null || true 203 + } 204 + trap cleanup EXIT 205 + 206 + echo "=== Building demos ===" 207 + bash "$SCRIPT_DIR/deploy.sh" --no-serve 208 + 209 + echo "" 210 + echo "=== Starting servers ===" 211 + 212 + # Page server on port 8080 213 + cd "$DOC_HTML/.." 214 + python3 -m http.server 8080 &>/dev/null & 215 + PAGE_PID=$! 216 + 217 + # CORS universe server on port 9090 218 + python3 "$SCRIPT_DIR/cors_server.py" 9090 "$CROSSORIGIN_DIR" &>/dev/null & 219 + CORS_PID=$! 220 + 221 + sleep 2 222 + echo " Page server: http://localhost:8080 (PID $PAGE_PID)" 223 + echo " CORS server: http://localhost:9090 (PID $CORS_PID)" 224 + 225 + # Verify both servers respond 226 + curl -sf http://localhost:8080/odoc-interactive-extension/demo5_crossorigin.html > /dev/null \ 227 + || { echo "FAIL: page server not responding"; exit 1; } 228 + curl -sf http://localhost:9090/universe/findlib_index.json > /dev/null \ 229 + || { echo "FAIL: CORS server not responding"; exit 1; } 230 + 231 + echo "" 232 + echo "=== Running Playwright test ===" 233 + node "$SCRIPT_DIR/test_crossorigin.js" 234 + ``` 235 + 236 + **Step 2: Write test_crossorigin.js (Playwright test)** 237 + 238 + ```javascript 239 + // test_crossorigin.js — Playwright test for cross-origin demo 240 + const { chromium } = require('playwright'); 241 + 242 + (async () => { 243 + const browser = await chromium.launch(); 244 + const page = await browser.newPage(); 245 + 246 + const consoleMessages = []; 247 + page.on('console', msg => consoleMessages.push(msg.text())); 248 + 249 + const errors = []; 250 + page.on('pageerror', err => errors.push(err.message)); 251 + 252 + console.log('Navigating to demo5_crossorigin.html...'); 253 + await page.goto('http://localhost:8080/odoc-interactive-extension/demo5_crossorigin.html'); 254 + 255 + // Wait for cells to render and execute (worker init + eval) 256 + console.log('Waiting for cells to execute...'); 257 + await page.waitForTimeout(15000); 258 + 259 + // Check cell outputs 260 + const cellOutputs = await page.evaluate(() => { 261 + const cells = document.querySelectorAll('x-ocaml'); 262 + return Array.from(cells).map((cell, i) => { 263 + const output = cell.querySelector('.jtw-output, .x-ocaml-output, [class*="output"]'); 264 + return { 265 + index: i, 266 + textContent: cell.textContent.substring(0, 200), 267 + }; 268 + }); 269 + }); 270 + 271 + console.log(`Found ${cellOutputs.length} cells`); 272 + 273 + // Check for cross-origin indicators in console 274 + const hasCrossOriginWorker = consoleMessages.some(m => 275 + m.includes('Starting worker') && m.includes('9090')); 276 + const hasCrossOriginFetch = consoleMessages.some(m => 277 + m.includes('Cross-origin import via fetch+eval')); 278 + const hasInitFinished = consoleMessages.some(m => 279 + m.includes('init() finished')); 280 + const hasSetupFinished = consoleMessages.some(m => 281 + m.includes('setup() finished')); 282 + 283 + // Check for init_error 284 + const hasInitError = consoleMessages.some(m => 285 + m.includes('init_error')); 286 + 287 + // Check for successful eval output (int = 7) 288 + const hasCorrectOutput = consoleMessages.some(m => 289 + m.includes('"stdout"') && m.includes('int = 7')) || 290 + consoleMessages.some(m => m.includes('int = 7')); 291 + 292 + // Also look for cell text content showing results 293 + const pageText = await page.evaluate(() => document.body.textContent); 294 + const hasResultInPage = pageText.includes('int = 7') || 295 + pageText.includes('Hello, Cross-Origin World'); 296 + 297 + console.log(''); 298 + console.log('=== Results ==='); 299 + console.log(` Worker loaded from port 9090: ${hasCrossOriginWorker ? 'YES' : 'NO'}`); 300 + console.log(` Cross-origin fetch+eval used: ${hasCrossOriginFetch ? 'YES' : 'NO'}`); 301 + console.log(` Init completed: ${hasInitFinished ? 'YES' : 'NO'}`); 302 + console.log(` Setup completed: ${hasSetupFinished ? 'YES' : 'NO'}`); 303 + console.log(` Init error: ${hasInitError ? 'YES (BAD)' : 'NO (good)'}`); 304 + console.log(` Correct output in page: ${hasResultInPage ? 'YES' : 'NO'}`); 305 + 306 + let passed = true; 307 + 308 + if (!hasInitFinished) { 309 + console.log('\nFAIL: Worker init did not complete'); 310 + passed = false; 311 + } 312 + if (!hasSetupFinished) { 313 + console.log('\nFAIL: Worker setup did not complete'); 314 + passed = false; 315 + } 316 + if (hasInitError) { 317 + console.log('\nFAIL: Worker reported init_error'); 318 + const errMsg = consoleMessages.find(m => m.includes('init_error')); 319 + console.log(` ${errMsg}`); 320 + passed = false; 321 + } 322 + if (!hasResultInPage) { 323 + console.log('\nFAIL: Expected output not found in page'); 324 + passed = false; 325 + } 326 + 327 + if (passed) { 328 + console.log('\nPASS: Cross-origin demo working correctly'); 329 + } else { 330 + console.log('\n--- Console messages (last 30) ---'); 331 + consoleMessages.slice(-30).forEach(m => console.log(` ${m.substring(0, 120)}`)); 332 + } 333 + 334 + await browser.close(); 335 + process.exit(passed ? 0 : 1); 336 + })(); 337 + ``` 338 + 339 + **Step 3: Make executable and ensure Playwright is available** 340 + 341 + Run: `chmod +x odoc-interactive-extension/test_crossorigin.sh` 342 + Run: `ls odoc-interactive-extension/node_modules/.package-lock.json 2>/dev/null || (cd odoc-interactive-extension && npm init -y && npm install playwright)` 343 + 344 + Note: If Playwright is already installed elsewhere (e.g., `x-ocaml/test/node_modules/`), the test script can reference that instead. Check with `ls x-ocaml/test/node_modules/playwright` first. 345 + 346 + **Step 4: Commit** 347 + 348 + ```bash 349 + git add odoc-interactive-extension/test_crossorigin.sh odoc-interactive-extension/test_crossorigin.js 350 + git commit -m "feat: add cross-origin test script and Playwright test" 351 + ``` 352 + 353 + --- 354 + 355 + ### Task 5: Run the full test and verify 356 + 357 + **Step 1: Deploy everything** 358 + 359 + Run: `bash odoc-interactive-extension/deploy.sh --no-serve` 360 + 361 + **Step 2: Start both servers** 362 + 363 + Run: `python3 -m http.server 8080 --directory _build/default/_doc/_html &` 364 + Run: `python3 odoc-interactive-extension/cors_server.py 9090 _build/default/_doc/_html/_crossorigin_universes &` 365 + 366 + **Step 3: Verify CORS headers** 367 + 368 + Run: `curl -sI http://localhost:9090/universe/findlib_index.json | grep -i access-control` 369 + Expected: `Access-Control-Allow-Origin: *` 370 + 371 + **Step 4: Open in Playwright and verify** 372 + 373 + Open `http://localhost:8080/odoc-interactive-extension/demo5_crossorigin.html` in Playwright. 374 + Wait 15 seconds for worker init + cell execution. 375 + Check that: 376 + - Console shows worker loaded from port 9090 URL 377 + - Console shows "Cross-origin import via fetch+eval" for .cma.js loading 378 + - Cells show correct output: `int = 7`, `Hello, Cross-Origin World`, JSON output 379 + 380 + **Step 5: Run the automated test** 381 + 382 + Run: `bash odoc-interactive-extension/test_crossorigin.sh` 383 + Expected: `PASS: Cross-origin demo working correctly` 384 + 385 + **Step 6: Commit** 386 + 387 + ```bash 388 + git commit --allow-empty -m "test: cross-origin demo verified working" 389 + ```
+28 -14
js_top_worker/bin/jtw.ml
··· 399 399 | _ -> None) hidden in 400 400 let prefixes = Util.StringSet.(of_list prefixes |> to_list) in 401 401 let d = relativize_or_fallback ~findlib_dir dir in 402 - (* Include pkg_path in dcs_url so it's correct relative to the HTTP root *) 402 + (* dcs_url is relative to the package's own findlib_index.json *) 403 403 let dcs = { 404 - Js_top_worker.Impl.dcs_url = Fpath.(v pkg_path / "lib" // d |> to_string); 404 + Js_top_worker.Impl.dcs_url = Fpath.(v "lib" // d |> to_string); 405 405 dcs_toplevel_modules = List.map String.capitalize_ascii non_hidden; 406 406 dcs_file_prefixes = prefixes; 407 407 } in ··· 450 450 let _ = Bos.OS.Dir.create ~path:true output_dir in 451 451 let findlib_dir = Ocamlfind.findlib_dir () |> Fpath.v in 452 452 453 - (* Build dependency map: package -> list of direct dependency paths *) 453 + (* Build dependency map: package -> list of dependency packages. 454 + Stdlib is implicitly required by everything, so add it for all 455 + non-stdlib packages even if ocamlfind doesn't list it. *) 454 456 let dep_map = Hashtbl.create 64 in 457 + let all_packages_set = Util.StringSet.of_list all_packages in 455 458 List.iter (fun pkg -> 456 459 let deps = match Ocamlfind.deps [pkg] with 457 - | Ok l -> List.filter (fun d -> d <> pkg) l (* Remove self from deps *) 460 + | Ok l -> List.filter (fun d -> d <> pkg) l 458 461 | Error _ -> [] 462 + in 463 + (* Add stdlib as implicit dependency for non-stdlib packages *) 464 + let deps = 465 + if pkg <> "stdlib" && not (List.mem "stdlib" deps) 466 + && Util.StringSet.mem "stdlib" all_packages_set 467 + then "stdlib" :: deps 468 + else deps 459 469 in 460 470 Hashtbl.add dep_map pkg deps) 461 471 all_packages; ··· 475 485 Hashtbl.add meta_path_map pkg_path full_meta_path) 476 486 pkg_results; 477 487 478 - (* Generate findlib_index for each package with correct META paths *) 488 + (* Generate findlib_index for each package. 489 + - meta_files: only this package's own META (relative to its own dir) 490 + - universes: relative paths to dependency package dirs (e.g., "../stdlib") *) 479 491 List.iter (fun (pkg_path, local_meta_path, deps) -> 480 - let this_meta = pkg_path ^ "/" ^ local_meta_path in 481 - let dep_metas = List.filter_map (fun dep -> 482 - match Hashtbl.find_opt meta_path_map dep with 483 - | Some path -> Some path 484 - | None -> 485 - Format.eprintf "Warning: no META path found for dep %s\n%!" dep; 486 - None) 492 + let dep_universes = List.filter_map (fun dep -> 493 + if Hashtbl.mem meta_path_map dep then 494 + Some ("../" ^ dep) 495 + else begin 496 + Format.eprintf "Warning: no universe found for dep %s\n%!" dep; 497 + None 498 + end) 487 499 deps 488 500 in 489 - let all_metas = this_meta :: dep_metas in 490 - let findlib_json = `Assoc [("meta_files", `List (List.map (fun s -> `String s) all_metas))] in 501 + let fields = [("meta_files", `List [`String local_meta_path])] in 502 + let fields = if dep_universes = [] then fields 503 + else fields @ [("universes", `List (List.map (fun s -> `String s) dep_universes))] in 504 + let findlib_json = `Assoc fields in 491 505 Out_channel.with_open_bin Fpath.(output_dir / pkg_path / "findlib_index.json" |> to_string) 492 506 (fun oc -> Printf.fprintf oc "%s\n" (Yojson.Safe.to_string findlib_json))) 493 507 pkg_results;
+13 -8
js_top_worker/idl/js_top_worker_client_msg.ml
··· 46 46 exception InitError of string 47 47 exception EvalError of string 48 48 49 + (** Use plain JSON.stringify/JSON.parse for cross-jsoo-version compatibility. *) 50 + let json_global : 'a Js_of_ocaml.Js.t = Js_of_ocaml.Js.Unsafe.pure_js_expr "JSON" 51 + let plain_stringify obj = json_global##stringify obj 52 + let plain_parse (s : Js_of_ocaml.Js.js_string Js_of_ocaml.Js.t) = json_global##parse s 53 + 49 54 (** Parse a worker message from JSON string *) 50 55 let parse_worker_msg s = 51 56 let open Js_of_ocaml in 52 - let obj = Json.unsafe_input (Js.string s) in 57 + let obj = plain_parse (Js.string s) in 53 58 let typ = Js.to_string (Js.Unsafe.get obj (Js.string "type")) in 54 59 let get_int key = Js.Unsafe.get obj (Js.string key) in 55 60 let get_string key = Js.to_string (Js.Unsafe.get obj (Js.string key)) in ··· 230 235 ("stdlib_dcs", Js.Unsafe.inject (match config.Msg.stdlib_dcs with Some s -> Js.some (Js.string s) | None -> Js.null)); 231 236 ("findlib_index", Js.Unsafe.inject (match config.Msg.findlib_index with Some s -> Js.some (Js.string s) | None -> Js.null)); 232 237 |] in 233 - Js.to_string (Json.output obj) 238 + Js.to_string (plain_stringify obj) 234 239 | `Eval (cell_id, env_id, code) -> 235 240 let obj = Js.Unsafe.obj [| 236 241 ("type", Js.Unsafe.inject (Js.string "eval")); ··· 238 243 ("env_id", Js.Unsafe.inject (Js.string env_id)); 239 244 ("code", Js.Unsafe.inject (Js.string code)); 240 245 |] in 241 - Js.to_string (Json.output obj) 246 + Js.to_string (plain_stringify obj) 242 247 | `Complete (cell_id, env_id, source, position, filename) -> 243 248 let pairs = [| 244 249 ("type", Js.Unsafe.inject (Js.string "complete")); ··· 251 256 | Some f -> Array.append pairs [| ("filename", Js.Unsafe.inject (Js.string f)) |] 252 257 | None -> pairs 253 258 in 254 - Js.to_string (Json.output (Js.Unsafe.obj pairs)) 259 + Js.to_string (plain_stringify (Js.Unsafe.obj pairs)) 255 260 | `TypeAt (cell_id, env_id, source, position, filename) -> 256 261 let pairs = [| 257 262 ("type", Js.Unsafe.inject (Js.string "type_at")); ··· 264 269 | Some f -> Array.append pairs [| ("filename", Js.Unsafe.inject (Js.string f)) |] 265 270 | None -> pairs 266 271 in 267 - Js.to_string (Json.output (Js.Unsafe.obj pairs)) 272 + Js.to_string (plain_stringify (Js.Unsafe.obj pairs)) 268 273 | `Errors (cell_id, env_id, source, filename) -> 269 274 let pairs = [| 270 275 ("type", Js.Unsafe.inject (Js.string "errors")); ··· 276 281 | Some f -> Array.append pairs [| ("filename", Js.Unsafe.inject (Js.string f)) |] 277 282 | None -> pairs 278 283 in 279 - Js.to_string (Json.output (Js.Unsafe.obj pairs)) 284 + Js.to_string (plain_stringify (Js.Unsafe.obj pairs)) 280 285 | `CreateEnv env_id -> 281 286 let obj = Js.Unsafe.obj [| 282 287 ("type", Js.Unsafe.inject (Js.string "create_env")); 283 288 ("env_id", Js.Unsafe.inject (Js.string env_id)); 284 289 |] in 285 - Js.to_string (Json.output obj) 290 + Js.to_string (plain_stringify obj) 286 291 | `DestroyEnv env_id -> 287 292 let obj = Js.Unsafe.obj [| 288 293 ("type", Js.Unsafe.inject (Js.string "destroy_env")); 289 294 ("env_id", Js.Unsafe.inject (Js.string env_id)); 290 295 |] in 291 - Js.to_string (Json.output obj) 296 + Js.to_string (plain_stringify obj) 292 297 in 293 298 Brr_worker.post t.worker (Js.string json) 294 299
+11 -2
js_top_worker/idl/message.ml
··· 95 95 96 96 (** {1 JSON helpers} *) 97 97 98 + (** Use plain JSON.stringify/JSON.parse instead of jsoo's Json.output/Json.unsafe_input. 99 + The jsoo versions use bytestring revivers that are incompatible across different 100 + jsoo versions (e.g., 6.0.1+ox vs 6.2.0). Since all values in our message objects 101 + are already proper JS values (created via Js.string, Js.Unsafe.inject, etc.), 102 + plain JSON works correctly and is cross-version compatible. *) 103 + let json_global : 'a Js.t = Js.Unsafe.pure_js_expr "JSON" 104 + let plain_stringify obj = json_global##stringify obj 105 + let plain_parse (s : Js.js_string Js.t) = json_global##parse s 106 + 98 107 let json_of_obj pairs = 99 108 Js.Unsafe.obj (Array.of_list (List.map (fun (k, v) -> (k, Js.Unsafe.inject v)) pairs)) 100 109 ··· 244 253 ("env_id", json_string env_id); 245 254 ] 246 255 in 247 - Js.to_string (Json.output obj) 256 + Js.to_string (plain_stringify obj) 248 257 249 258 (** {1 Client message parsing} *) 250 259 ··· 256 265 } 257 266 258 267 let client_msg_of_string s = 259 - let obj = Json.unsafe_input (Js.string s) in 268 + let obj = plain_parse (Js.string s) in 260 269 let typ = get_string obj "type" in 261 270 match typ with 262 271 | "init" ->
+12 -59
js_top_worker/lib/dune
··· 1 - ; Worker library -- upstream OCaml 1 + ; Generate impl.ml from impl.cppo.ml with conditional OXCAML flag 2 2 3 - (library 4 - (public_name js_top_worker) 3 + (rule 4 + (targets impl.ml) 5 + (deps (:x impl.cppo.ml)) 5 6 (enabled_if (not %{ocaml-config:ox})) 6 - (modules toplexer ocamltop impl environment) 7 - (libraries 8 - logs 9 - lwt 10 - js_of_ocaml-compiler 11 - js_of_ocaml-ppx 12 - astring 13 - mime_printer 14 - compiler-libs.common 15 - compiler-libs.toplevel 16 - merlin-lib.kernel 17 - merlin-lib.utils 18 - merlin-lib.query_protocol 19 - merlin-lib.query_commands 20 - merlin-lib.ocaml_parsing 21 - ppxlib 22 - ppx_deriving.api) 23 - (js_of_ocaml 24 - (javascript_files stubs.js)) 25 - (preprocess 26 - (per_module 27 - ((action 28 - (run %{bin:cppo} -V OCAML:%{ocaml_version} %{input-file})) 29 - impl)))) 7 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} %{x} -o %{targets}))) 30 8 31 - ; Worker library -- OxCaml 9 + (rule 10 + (targets impl.ml) 11 + (deps (:x impl.cppo.ml)) 12 + (enabled_if %{ocaml-config:ox}) 13 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{x} -o %{targets}))) 32 14 33 15 (library 34 16 (public_name js_top_worker) 35 - (enabled_if %{ocaml-config:ox}) 36 17 (modules toplexer ocamltop impl environment) 37 18 (libraries 38 19 logs ··· 51 32 ppxlib 52 33 ppx_deriving.api) 53 34 (js_of_ocaml 54 - (javascript_files stubs.js)) 55 - (preprocess 56 - (per_module 57 - ((action 58 - (run %{bin:cppo} -V OCAML:%{ocaml_version} -D "OXCAML" %{input-file})) 59 - impl)))) 35 + (javascript_files stubs.js))) 60 36 61 37 (ocamllex toplexer) 62 38 63 - ; Web worker library -- upstream OCaml 39 + ; Web worker library (no OxCaml differences) 64 40 65 41 (library 66 42 (public_name js_top_worker-web) 67 43 (name js_top_worker_web) 68 - (enabled_if (not %{ocaml-config:ox})) 69 - (modules worker findlibish jslib) 70 - (preprocess 71 - (pps js_of_ocaml-ppx)) 72 - (libraries 73 - js_top_worker 74 - js_top_worker-rpc.message 75 - js_of_ocaml-ppx 76 - js_of_ocaml-toplevel 77 - js_of_ocaml-lwt 78 - logs.browser 79 - uri 80 - angstrom 81 - findlib 82 - fpath 83 - yojson)) 84 - 85 - ; Web worker library -- OxCaml 86 - 87 - (library 88 - (public_name js_top_worker-web) 89 - (name js_top_worker_web) 90 - (enabled_if %{ocaml-config:ox}) 91 44 (modules worker findlibish jslib) 92 45 (preprocess 93 46 (pps js_of_ocaml-ppx))
+27 -2
js_top_worker/lib/findlibish.ml
··· 258 258 in 259 259 (* Load META files from this universe *) 260 260 let* local_libs = Lwt_list.filter_map_p (load_meta async_get) resolved_metas in 261 - (* Resolve universe paths from root (they're already full paths) *) 261 + (* Resolve universe paths relative to this findlib_index's directory. 262 + Universe paths can be relative (e.g., "../stdlib") or absolute from 263 + root (e.g., "packages/stdlib"). Relative paths are the common case 264 + from opam-all; absolute paths are kept for backward compatibility. *) 262 265 let universe_index_urls = 263 266 List.map (fun u -> 264 - resolve_from_root ~base:index_url (Filename.concat u "findlib_index.json")) 267 + let index_path = Filename.concat u "findlib_index.json" in 268 + if String.length u > 0 && u.[0] = '.' then 269 + (* Relative path (e.g., "../stdlib") — resolve from this index's dir *) 270 + resolve_relative_to_dir ~base:index_url index_path 271 + else 272 + (* Absolute-from-root path — legacy behavior *) 273 + resolve_from_root ~base:index_url index_path) 265 274 universes 266 275 in 267 276 let* universe_libs = Lwt_list.map_p load_universe universe_index_urls in ··· 328 337 dcss) 329 338 in 330 339 List.fold_left require [] packages 340 + 341 + let find_dcs_url v package_name = 342 + match List.find_opt (fun lib -> lib.name = package_name) v with 343 + | None -> None 344 + | Some lib -> 345 + let path = Fpath.(v (Uri.path lib.meta_uri) |> parent) in 346 + let dir = 347 + match lib.dir with 348 + | None -> path 349 + | Some "+" -> Fpath.parent path 350 + | Some d when String.length d > 0 && d.[0] = '^' -> 351 + Fpath.parent path 352 + | Some d -> Fpath.(path // v d) 353 + in 354 + let dcs = Fpath.(dir / dcs_filename |> to_string) in 355 + Some (Uri.with_path lib.meta_uri dcs |> Uri.to_string)
+34
js_top_worker/lib/findlibish.mli
··· 1 + (** Lightweight findlib for the browser. 2 + 3 + Parses META files fetched over HTTP and manages package loading 4 + for the js_of_ocaml toplevel worker. *) 5 + 6 + (** Opaque type representing a loaded set of findlib libraries. *) 7 + type t 8 + 9 + (** Initialize findlib by fetching and parsing a findlib_index file. 10 + Follows universe links to transitively load all META files. *) 11 + val init : 12 + (string -> (string, [ `Msg of string ]) result Lwt.t) -> 13 + string -> 14 + t Lwt.t 15 + 16 + (** Fetch dynamic CMI information from the given URL. *) 17 + val fetch_dynamic_cmis : 18 + (string -> string option) -> 19 + string -> 20 + (Js_top_worker_rpc.Toplevel_api_gen.dynamic_cmis, [ `Msg of string ]) result 21 + 22 + (** Load the named packages and their transitive dependencies. 23 + Returns the dynamic CMI descriptors for all newly loaded packages. *) 24 + val require : 25 + import_scripts:(string list -> unit) -> 26 + (string -> string option) -> 27 + bool -> 28 + t -> 29 + string list -> 30 + Js_top_worker_rpc.Toplevel_api_gen.dynamic_cmis list 31 + 32 + (** Find the dynamic_cmis.json URL for a named package. 33 + Returns [None] if the package is not in the library list. *) 34 + val find_dcs_url : t -> string -> string option
+38 -8
js_top_worker/lib/impl.ml js_top_worker/lib/impl.cppo.ml
··· 274 274 val import_scripts : string list -> unit 275 275 val init_function : string -> unit -> unit 276 276 val get_stdlib_dcs : string -> dynamic_cmis list 277 + val find_stdlib_dcs : findlib_t -> dynamic_cmis list 277 278 val findlib_init : string -> findlib_t Lwt.t 278 279 val path : string 279 280 ··· 422 423 let info = { Toploop.section = "Findlib"; doc = "Load a package (js_top_worker)" } in 423 424 Toploop.add_directive "require" (Toploop.Directive_string require_handler) info 424 425 426 + (* Merlin-lib's Load_path.reset asserts Ocaml_utils.Local_store.is_bound(). 427 + This is a DIFFERENT Local_store from compiler-libs' Local_store — they 428 + have separate mutable state. We must bind the merlin-lib one. *) 429 + let the_store = lazy (Ocaml_utils.Local_store.fresh ()) 430 + 425 431 let setup functions () = 426 432 let stdout_buff = Buffer.create 100 in 427 433 let stderr_buff = Buffer.create 100 in ··· 442 448 match !path with Some p -> p | None -> failwith "Path not set" 443 449 in 444 450 451 + (* Bind the local store around the entire setup so that 452 + merlin-lib's Load_path assertions (is_bound) are satisfied. 453 + All Load_path operations (dir_directory, initialize_toplevel_env, 454 + exec' "open Stdlib", etc.) need the store bound. *) 455 + let store = Lazy.force the_store in 456 + Ocaml_utils.Local_store.with_store store (fun () -> 445 457 Topdirs.dir_directory path; 446 458 447 459 Toploop.initialize_toplevel_env (); ··· 453 465 exec' "#enable \"pretty\";;"; 454 466 exec' "#disable \"shortvar\";;"; 455 467 Sys.interactive := true; 456 - Logs.info (fun m -> m "Setup complete"); 468 + Logs.info (fun m -> m "Setup complete")); 457 469 { 458 470 stdout = Buffer.contents stdout_buff; 459 471 stderr = Buffer.contents stderr_buff; ··· 508 520 if l >= 2 && String.sub phrase (l - 2) 2 = ";;" then phrase 509 521 else phrase ^ ";;" 510 522 in 523 + (* Bind the merlin-lib local store so Toploop.execute_phrase can access 524 + Load_path without hitting merlin-lib assertions. *) 525 + let store = Lazy.force the_store in 511 526 let o, () = 527 + Ocaml_utils.Local_store.with_store store (fun () -> 512 528 Environment.with_env env (fun () -> 513 529 S.capture 514 530 (fun () -> ··· 527 543 done 528 544 with End_of_file -> ()); 529 545 flush_all ()) 530 - ()) 546 + ())) 531 547 in 532 548 let mime_vals = Mime_printer.get () in 533 549 Format.pp_print_flush pp_code (); ··· 572 588 if l >= 2 && String.sub phrase (l - 2) 2 = ";;" then phrase 573 589 else phrase ^ ";;" 574 590 in 591 + (* Bind the merlin-lib local store so Toploop.execute_phrase can access 592 + Load_path without hitting merlin-lib assertions. *) 593 + let store = Lazy.force the_store in 575 594 let o, () = 595 + Ocaml_utils.Local_store.with_store store (fun () -> 576 596 Environment.with_env env (fun () -> 577 597 S.capture 578 598 (fun () -> ··· 607 627 done 608 628 with End_of_file -> ()); 609 629 flush_all ()) 610 - ()) 630 + ())) 611 631 in 612 632 (* Get any remaining mime_vals (shouldn't be any after last callback) *) 613 633 let mime_vals = Mime_printer.get () in ··· 760 780 path := Some S.path; 761 781 762 782 let findlib_path = Option.value ~default:"findlib_index.json" init_libs.findlib_index in 763 - findlib_v := Some (S.findlib_init findlib_path); 783 + let findlib_promise = S.findlib_init findlib_path in 784 + findlib_v := Some findlib_promise; 764 785 765 786 let stdlib_dcs = 766 787 match init_libs.stdlib_dcs with ··· 770 791 let* () = 771 792 match S.get_stdlib_dcs stdlib_dcs with 772 793 | [ dcs ] -> add_dynamic_cmis dcs 773 - | _ -> Lwt.return () 794 + | _ -> 795 + (* stdlib_dcs not found at expected path (e.g. multiverse layout). 796 + Await findlib resolution to discover stdlib via universe links. *) 797 + Logs.info (fun m -> m "stdlib_dcs not found at %s, trying findlib discovery" stdlib_dcs); 798 + let* v = findlib_promise in 799 + (match S.find_stdlib_dcs v with 800 + | [ dcs ] -> add_dynamic_cmis dcs 801 + | _ -> 802 + Logs.info (fun m -> m "stdlib not found in findlib either"); 803 + Lwt.return ()) 774 804 in 775 805 #if defined OXCAML 776 806 Language_extension.(set_universe_and_enable_all Universe.Beta); ··· 1135 1165 let unit_info = Unit_info.make ~source_file:filename Impl prefix in 1136 1166 #endif 1137 1167 try 1138 - let store = Local_store.fresh () in 1139 - Local_store.with_store store (fun () -> 1140 - Local_store.reset (); 1168 + let store = Ocaml_utils.Local_store.fresh () in 1169 + Ocaml_utils.Local_store.with_store store (fun () -> 1170 + Ocaml_utils.Local_store.reset (); 1141 1171 let env = 1142 1172 Typemod.initial_env ~loc ~initially_opened_module:(Some "Stdlib") 1143 1173 ~open_implicit_modules:dep_modules
+6
js_top_worker/lib/jslib.ml
··· 11 11 in 12 12 Option.map Js.to_string x 13 13 in 14 + let has_scheme = 15 + let len = String.length url in 16 + (len >= 7 && String.sub url 0 7 = "http://") || 17 + (len >= 8 && String.sub url 0 8 = "https://") 18 + in 14 19 match global_rel_url with 20 + | _ when has_scheme -> url 15 21 | Some rel -> 16 22 (* If url starts with /, it's relative to server root - just use the scheme/host *) 17 23 if String.length url > 0 && url.[0] = '/' then
+26
js_top_worker/lib/worker.ml
··· 57 57 let get_stdlib_dcs uri = 58 58 Findlibish.fetch_dynamic_cmis sync_get uri |> Result.to_list 59 59 60 + let find_stdlib_dcs v = 61 + (* Try "stdlib" first (standard META name), fall back to "ocaml" *) 62 + let pkg = match Findlibish.find_dcs_url v "stdlib" with 63 + | Some _ as r -> r 64 + | None -> Findlibish.find_dcs_url v "ocaml" 65 + in 66 + match pkg with 67 + | Some url -> 68 + Jslib.log "Found stdlib dcs via findlib: %s" url; 69 + (match Findlibish.fetch_dynamic_cmis sync_get url with 70 + | Ok dcs -> 71 + (* The dcs_url in the JSON is relative to the package dir. 72 + Rewrite it to be absolute using the dynamic_cmis.json URL 73 + parent directory. *) 74 + let abs_dcs_url = 75 + match String.rindex_opt url '/' with 76 + | Some i -> String.sub url 0 (i + 1) 77 + | None -> url 78 + in 79 + Jslib.log "Rewriting dcs_url from %s to %s" dcs.Js_top_worker_rpc.Toplevel_api_gen.dcs_url abs_dcs_url; 80 + [{ dcs with Js_top_worker_rpc.Toplevel_api_gen.dcs_url = abs_dcs_url }] 81 + | Error _ -> []) 82 + | None -> 83 + Jslib.log "stdlib not found in findlib (tried 'stdlib' and 'ocaml')"; 84 + [] 85 + 60 86 let import_scripts urls = 61 87 (* Map relative URLs to absolute using the global base URL *) 62 88 let absolute_urls = List.map Jslib.map_url urls in
+1
js_top_worker/test/browser/test_worker.ml
··· 26 26 let async_get _ = Lwt.return (Error (`Msg "Not implemented")) 27 27 let create_file = Js_of_ocaml.Sys_js.create_file 28 28 let get_stdlib_dcs _ = [] 29 + let find_stdlib_dcs _ = [] 29 30 let import_scripts _ = () 30 31 let findlib_init _ = Lwt.return () 31 32 let require _ () _ = []
+11
js_top_worker/test/node/node_dependency_test.ml
··· 73 73 Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get uri 74 74 |> Result.to_list 75 75 76 + let find_stdlib_dcs v = 77 + let pkg = match Js_top_worker_web.Findlibish.find_dcs_url v "stdlib" with 78 + | Some _ as r -> r 79 + | None -> Js_top_worker_web.Findlibish.find_dcs_url v "ocaml" 80 + in 81 + match pkg with 82 + | Some url -> 83 + Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get url 84 + |> Result.to_list 85 + | None -> [] 86 + 76 87 let require b v = function 77 88 | [] -> [] 78 89 | packages ->
+11
js_top_worker/test/node/node_directive_test.ml
··· 88 88 Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get uri 89 89 |> Result.to_list 90 90 91 + let find_stdlib_dcs v = 92 + let pkg = match Js_top_worker_web.Findlibish.find_dcs_url v "stdlib" with 93 + | Some _ as r -> r 94 + | None -> Js_top_worker_web.Findlibish.find_dcs_url v "ocaml" 95 + in 96 + match pkg with 97 + | Some url -> 98 + Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get url 99 + |> Result.to_list 100 + | None -> [] 101 + 91 102 let require b v = function 92 103 | [] -> [] 93 104 | packages ->
+11
js_top_worker/test/node/node_env_test.ml
··· 73 73 Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get uri 74 74 |> Result.to_list 75 75 76 + let find_stdlib_dcs v = 77 + let pkg = match Js_top_worker_web.Findlibish.find_dcs_url v "stdlib" with 78 + | Some _ as r -> r 79 + | None -> Js_top_worker_web.Findlibish.find_dcs_url v "ocaml" 80 + in 81 + match pkg with 82 + | Some url -> 83 + Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get url 84 + |> Result.to_list 85 + | None -> [] 86 + 76 87 let require b v = function 77 88 | [] -> [] 78 89 | packages ->
+11
js_top_worker/test/node/node_incremental_test.ml
··· 64 64 Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get uri 65 65 |> Result.to_list 66 66 67 + let find_stdlib_dcs v = 68 + let pkg = match Js_top_worker_web.Findlibish.find_dcs_url v "stdlib" with 69 + | Some _ as r -> r 70 + | None -> Js_top_worker_web.Findlibish.find_dcs_url v "ocaml" 71 + in 72 + match pkg with 73 + | Some url -> 74 + Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get url 75 + |> Result.to_list 76 + | None -> [] 77 + 67 78 let require b v = function 68 79 | [] -> [] 69 80 | packages -> Js_top_worker_web.Findlibish.require ~import_scripts sync_get b v packages
+11
js_top_worker/test/node/node_mime_test.ml
··· 75 75 Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get uri 76 76 |> Result.to_list 77 77 78 + let find_stdlib_dcs v = 79 + let pkg = match Js_top_worker_web.Findlibish.find_dcs_url v "stdlib" with 80 + | Some _ as r -> r 81 + | None -> Js_top_worker_web.Findlibish.find_dcs_url v "ocaml" 82 + in 83 + match pkg with 84 + | Some url -> 85 + Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get url 86 + |> Result.to_list 87 + | None -> [] 88 + 78 89 let require b v = function 79 90 | [] -> [] 80 91 | packages ->
+11
js_top_worker/test/node/node_ppx_test.ml
··· 76 76 Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get uri 77 77 |> Result.to_list 78 78 79 + let find_stdlib_dcs v = 80 + let pkg = match Js_top_worker_web.Findlibish.find_dcs_url v "stdlib" with 81 + | Some _ as r -> r 82 + | None -> Js_top_worker_web.Findlibish.find_dcs_url v "ocaml" 83 + in 84 + match pkg with 85 + | Some url -> 86 + Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get url 87 + |> Result.to_list 88 + | None -> [] 89 + 79 90 let require b v = function 80 91 | [] -> [] 81 92 | packages -> Js_top_worker_web.Findlibish.require ~import_scripts sync_get b v packages
+11
js_top_worker/test/node/node_test.ml
··· 65 65 Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get uri 66 66 |> Result.to_list 67 67 68 + let find_stdlib_dcs v = 69 + let pkg = match Js_top_worker_web.Findlibish.find_dcs_url v "stdlib" with 70 + | Some _ as r -> r 71 + | None -> Js_top_worker_web.Findlibish.find_dcs_url v "ocaml" 72 + in 73 + match pkg with 74 + | Some url -> 75 + Js_top_worker_web.Findlibish.fetch_dynamic_cmis sync_get url 76 + |> Result.to_list 77 + | None -> [] 78 + 68 79 let require b v = function 69 80 | [] -> [] 70 81 | packages -> Js_top_worker_web.Findlibish.require ~import_scripts sync_get b v packages
+1
js_top_worker/test/unix/unix_test.ml
··· 73 73 let init_function _ () = failwith "Not implemented" 74 74 let findlib_init _ = Lwt.return () 75 75 let get_stdlib_dcs _uri = [] 76 + let find_stdlib_dcs _ = [] 76 77 77 78 let require _ () packages = 78 79 try
+4 -4
odoc-scrollycode-extension/src/scrollycode_extension.ml
··· 1 1 (** Scrollycode Extension for odoc 2 2 3 3 Provides scroll-driven code tutorials. Theme styling is handled 4 - externally via CSS custom properties defined in {!Scrollycode_css} 5 - and set by theme files in {!Scrollycode_themes}. 4 + externally via CSS custom properties defined in [Scrollycode_css] 5 + and set by theme files in [Scrollycode_themes]. 6 6 7 7 Authoring format uses [@scrolly] custom tags with an ordered 8 8 list inside, where each list item is a tutorial step containing 9 9 a bold title, prose paragraphs, and a code block. 10 10 11 - For backward compatibility, @scrolly.warm / @scrolly.dark / 12 - @scrolly.notebook are still accepted but the theme suffix is 11 + For backward compatibility, \@scrolly.warm / \@scrolly.dark / 12 + \@scrolly.notebook are still accepted but the theme suffix is 13 13 ignored — theme selection is now a CSS concern. *) 14 14 15 15 module Comment = Odoc_model.Comment
+3 -3
odoc/dune-project
··· 35 35 (package 36 36 (name odoc) 37 37 (sites (lib extensions)) 38 - (documentation (depends odoc-driver odoc-parser odoc-md sherlodoc cmdliner))) 38 + ) 39 39 40 - (package (name odoc-parser) (documentation (depends odoc))) 40 + (package (name odoc-parser)) 41 41 (package (name odoc-md)) 42 - (package (name odoc-driver) (documentation (depends sherlodoc odoc))) 42 + (package (name odoc-driver)) 43 43 (package (name odoc-bench) (allow_empty)) 44 44 (package (name sherlodoc)) 45 45
+4
odoc/src/.ocamlformat-ignore
··· 1 1 document/*.cppo.ml 2 2 loader/*.cppo.ml 3 + loader/*.cppo.mli 3 4 loader/cmi.ml 4 5 loader/cmi.mli 5 6 loader/cmt.ml 6 7 loader/cmti.ml 7 8 loader/doc_attr.ml 9 + loader/ident_env.ml 8 10 loader/implementation.ml 11 + loader/odoc_loader.ml 9 12 loader/typedtree_traverse.ml 10 13 loader/lookup_def.ml 11 14 loader/lookup_def.mli 15 + syntax_highlighter/*.cppo.ml 12 16 syntax_highlighter/syntax_highlighter.ml 13 17 model/*.cppo.ml 14 18 odoc/*.cppo.ml
odoc/src/loader/cmi.ml odoc/src/loader/cmi.cppo.ml
odoc/src/loader/cmi.mli odoc/src/loader/cmi.cppo.mli
odoc/src/loader/cmt.ml odoc/src/loader/cmt.cppo.ml
odoc/src/loader/cmti.ml odoc/src/loader/cmti.cppo.ml
odoc/src/loader/doc_attr.ml odoc/src/loader/doc_attr.cppo.ml
+107 -19
odoc/src/loader/dune
··· 1 - (library 2 - (name odoc_loader) 3 - (public_name odoc.loader) 4 - (enabled_if 5 - (not %{ocaml-config:ox})) 6 - (preprocess 7 - (action 8 - (run %{bin:cppo} -V OCAML:%{ocaml_version} %{input-file}))) 9 - (libraries 10 - odoc_model 11 - odoc-parser 12 - syntax_highlighter 13 - odoc_document 14 - odoc_utils 15 - compiler-libs.optcomp)) 1 + (rule 2 + (targets cmi.ml) 3 + (deps (:x cmi.cppo.ml)) 4 + (enabled_if (not %{ocaml-config:ox})) 5 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} %{x} -o %{targets}))) 6 + 7 + (rule 8 + (targets cmi.ml) 9 + (deps (:x cmi.cppo.ml)) 10 + (enabled_if %{ocaml-config:ox}) 11 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{x} -o %{targets}))) 12 + 13 + (rule 14 + (targets cmi.mli) 15 + (deps (:x cmi.cppo.mli)) 16 + (enabled_if (not %{ocaml-config:ox})) 17 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} %{x} -o %{targets}))) 18 + 19 + (rule 20 + (targets cmi.mli) 21 + (deps (:x cmi.cppo.mli)) 22 + (enabled_if %{ocaml-config:ox}) 23 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{x} -o %{targets}))) 24 + 25 + (rule 26 + (targets cmti.ml) 27 + (deps (:x cmti.cppo.ml)) 28 + (enabled_if (not %{ocaml-config:ox})) 29 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} %{x} -o %{targets}))) 30 + 31 + (rule 32 + (targets cmti.ml) 33 + (deps (:x cmti.cppo.ml)) 34 + (enabled_if %{ocaml-config:ox}) 35 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{x} -o %{targets}))) 36 + 37 + (rule 38 + (targets cmt.ml) 39 + (deps (:x cmt.cppo.ml)) 40 + (enabled_if (not %{ocaml-config:ox})) 41 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} %{x} -o %{targets}))) 42 + 43 + (rule 44 + (targets cmt.ml) 45 + (deps (:x cmt.cppo.ml)) 46 + (enabled_if %{ocaml-config:ox}) 47 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{x} -o %{targets}))) 48 + 49 + (rule 50 + (targets doc_attr.ml) 51 + (deps (:x doc_attr.cppo.ml)) 52 + (enabled_if (not %{ocaml-config:ox})) 53 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} %{x} -o %{targets}))) 54 + 55 + (rule 56 + (targets doc_attr.ml) 57 + (deps (:x doc_attr.cppo.ml)) 58 + (enabled_if %{ocaml-config:ox}) 59 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{x} -o %{targets}))) 60 + 61 + (rule 62 + (targets ident_env.ml) 63 + (deps (:x ident_env.cppo.ml)) 64 + (enabled_if (not %{ocaml-config:ox})) 65 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} %{x} -o %{targets}))) 66 + 67 + (rule 68 + (targets ident_env.ml) 69 + (deps (:x ident_env.cppo.ml)) 70 + (enabled_if %{ocaml-config:ox}) 71 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{x} -o %{targets}))) 72 + 73 + (rule 74 + (targets implementation.ml) 75 + (deps (:x implementation.cppo.ml)) 76 + (enabled_if (not %{ocaml-config:ox})) 77 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} %{x} -o %{targets}))) 78 + 79 + (rule 80 + (targets implementation.ml) 81 + (deps (:x implementation.cppo.ml)) 82 + (enabled_if %{ocaml-config:ox}) 83 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{x} -o %{targets}))) 84 + 85 + (rule 86 + (targets odoc_loader.ml) 87 + (deps (:x odoc_loader.cppo.ml)) 88 + (enabled_if (not %{ocaml-config:ox})) 89 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} %{x} -o %{targets}))) 90 + 91 + (rule 92 + (targets odoc_loader.ml) 93 + (deps (:x odoc_loader.cppo.ml)) 94 + (enabled_if %{ocaml-config:ox}) 95 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{x} -o %{targets}))) 96 + 97 + (rule 98 + (targets typedtree_traverse.ml) 99 + (deps (:x typedtree_traverse.cppo.ml)) 100 + (enabled_if (not %{ocaml-config:ox})) 101 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} %{x} -o %{targets}))) 102 + 103 + (rule 104 + (targets typedtree_traverse.ml) 105 + (deps (:x typedtree_traverse.cppo.ml)) 106 + (enabled_if %{ocaml-config:ox}) 107 + (action (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{x} -o %{targets}))) 16 108 17 109 (library 18 110 (name odoc_loader) 19 111 (public_name odoc.loader) 20 - (enabled_if %{ocaml-config:ox}) 21 - (preprocess 22 - (action 23 - (run %{bin:cppo} -V OCAML:%{ocaml_version} -D "OXCAML" %{input-file}))) 24 112 (libraries 25 113 odoc_model 26 114 odoc-parser
odoc/src/loader/ident_env.ml odoc/src/loader/ident_env.cppo.ml
odoc/src/loader/implementation.ml odoc/src/loader/implementation.cppo.ml
odoc/src/loader/odoc_loader.ml odoc/src/loader/odoc_loader.cppo.ml
odoc/src/loader/typedtree_traverse.ml odoc/src/loader/typedtree_traverse.cppo.ml
+1 -1
odoc/src/parser/lexer.mll
··· 549 549 { warning input Parse_error.truncated_see; 550 550 emit input (`Word "@see") } 551 551 552 - | '@' (['a'-'z' 'A'-'Z'] ['a'-'z' 'A'-'Z' '0'-'9' '_' '.']* as tag) 552 + | '@' (['a'-'z' 'A'-'Z'] ['a'-'z' 'A'-'Z' '0'-'9' '_' '.' '-']* as tag) 553 553 { emit input (`Tag (`Custom tag)) } 554 554 555 555 | '@'
+14 -11
odoc/src/syntax_highlighter/dune
··· 1 - (library 2 - (name syntax_highlighter) 3 - (public_name odoc.syntax_highlighter) 1 + (rule 2 + (targets syntax_highlighter.ml) 3 + (deps 4 + (:x syntax_highlighter.cppo.ml)) 4 5 (enabled_if 5 6 (not %{ocaml-config:ox})) 6 - (preprocess 7 - (action 8 - (run %{bin:cppo} -V OCAML:%{ocaml_version} %{input-file}))) 9 - (libraries compiler-libs.common)) 7 + (action 8 + (run %{bin:cppo} -V OCAML:%{ocaml_version} %{x} -o %{targets}))) 9 + 10 + (rule 11 + (targets syntax_highlighter.ml) 12 + (deps 13 + (:x syntax_highlighter.cppo.ml)) 14 + (enabled_if %{ocaml-config:ox}) 15 + (action 16 + (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{x} -o %{targets}))) 10 17 11 18 (library 12 19 (name syntax_highlighter) 13 20 (public_name odoc.syntax_highlighter) 14 - (enabled_if %{ocaml-config:ox}) 15 - (preprocess 16 - (action 17 - (run %{bin:cppo} -V OCAML:%{ocaml_version} -D OXCAML %{input-file}))) 18 21 (libraries compiler-libs.common))
odoc/src/syntax_highlighter/syntax_highlighter.ml odoc/src/syntax_highlighter/syntax_highlighter.cppo.ml
+284
pkgs
··· 1 + angstrom 2 + angstrom.async 3 + angstrom.lwt-unix 4 + angstrom.unix 5 + astring 6 + astring.top 7 + base64 8 + base64.rfc2045 9 + basement 10 + bigstringaf 11 + bos 12 + bos.setup 13 + bos.top 14 + brr 15 + brr.ocaml_poke 16 + brr.ocaml_poke_ui 17 + brr.poke 18 + brr.poked 19 + bytes 20 + camlp-streams 21 + capsule0 22 + capsule0.blocking_sync 23 + capsule0.expert 24 + chrome-trace 25 + cmdliner 26 + compiler-libs 27 + compiler-libs.bytecomp 28 + compiler-libs.common 29 + compiler-libs.optcomp 30 + compiler-libs.toplevel 31 + cppo 32 + crunch 33 + csexp 34 + cstruct 35 + domain-local-await 36 + dune 37 + dune-action-plugin 38 + dune-build-info 39 + dune-configurator 40 + dune-glob 41 + dune-private-libs 42 + dune-private-libs.dune-section 43 + dune-private-libs.meta_parser 44 + dune-rpc 45 + dune-rpc.private 46 + dune-site 47 + dune-site.dynlink 48 + dune-site.linker 49 + dune-site.plugins 50 + dune-site.private 51 + dune-site.toplevel 52 + dune.configurator 53 + dyn 54 + dynlink 55 + eio 56 + eio.core 57 + eio.mock 58 + eio.runtime_events 59 + eio.unix 60 + eio.utils 61 + eio_linux 62 + eio_main 63 + eio_posix 64 + either 65 + findlib 66 + findlib.dynload 67 + findlib.internal 68 + findlib.top 69 + fix 70 + fmt 71 + fmt.cli 72 + fmt.top 73 + fmt.tty 74 + fpath 75 + fpath.top 76 + fs-io 77 + gen 78 + hmap 79 + iomux 80 + jane-street-headers 81 + js_of_ocaml 82 + js_of_ocaml-compiler 83 + js_of_ocaml-compiler.dynlink 84 + js_of_ocaml-compiler.findlib-support 85 + js_of_ocaml-compiler.runtime 86 + js_of_ocaml-compiler.runtime-files 87 + js_of_ocaml-lwt 88 + js_of_ocaml-ppx 89 + js_of_ocaml-ppx.as-lib 90 + js_of_ocaml-toplevel 91 + js_of_ocaml.deriving 92 + js_top_worker 93 + js_top_worker-bin 94 + js_top_worker-rpc 95 + js_top_worker-rpc.message 96 + js_top_worker-web 97 + jsonm 98 + logs 99 + logs.browser 100 + logs.cli 101 + logs.fmt 102 + logs.lwt 103 + logs.threaded 104 + logs.top 105 + lwt 106 + lwt-dllist 107 + lwt.unix 108 + mdx 109 + mdx.__private__ 110 + mdx.__private__.odoc_parser 111 + mdx.test 112 + mdx.top 113 + menhir 114 + menhirCST 115 + menhirGLR 116 + menhirLib 117 + menhirSdk 118 + merlin-lib 119 + merlin-lib.analysis 120 + merlin-lib.commands 121 + merlin-lib.config 122 + merlin-lib.dot_protocol 123 + merlin-lib.extend 124 + merlin-lib.index_format 125 + merlin-lib.kernel 126 + merlin-lib.ocaml_merlin_specific 127 + merlin-lib.ocaml_parsing 128 + merlin-lib.ocaml_preprocess 129 + merlin-lib.ocaml_typing 130 + merlin-lib.ocaml_utils 131 + merlin-lib.os_ipc 132 + merlin-lib.query_commands 133 + merlin-lib.query_protocol 134 + merlin-lib.sherlodoc 135 + merlin-lib.utils 136 + mime_printer 137 + mtime 138 + mtime.clock 139 + mtime.clock.os 140 + mtime.top 141 + ocaml-compiler-libs 142 + ocaml-compiler-libs.bytecomp 143 + ocaml-compiler-libs.common 144 + ocaml-compiler-libs.optcomp 145 + ocaml-compiler-libs.shadow 146 + ocaml-compiler-libs.toplevel 147 + ocaml-syntax-shims 148 + ocaml-version 149 + ocaml_intrinsics_kernel 150 + ocamlbuild 151 + ocamlc-loc 152 + ocamlgraph 153 + ocp-indent 154 + ocp-indent.dynlink 155 + ocp-indent.lexer 156 + ocp-indent.lib 157 + ocp-indent.utils 158 + ocplib-endian 159 + ocplib-endian.bigstring 160 + odoc-parser 161 + opam-core 162 + opam-core.cmdliner 163 + opam-file-format 164 + opam-format 165 + optint 166 + ordering 167 + patch 168 + pp 169 + ppx_array_base 170 + ppx_base 171 + ppx_blob 172 + ppx_cold 173 + ppx_compare 174 + ppx_compare.expander 175 + ppx_compare.runtime-lib 176 + ppx_derivers 177 + ppx_deriving 178 + ppx_deriving.api 179 + ppx_deriving.create 180 + ppx_deriving.enum 181 + ppx_deriving.eq 182 + ppx_deriving.fold 183 + ppx_deriving.iter 184 + ppx_deriving.make 185 + ppx_deriving.map 186 + ppx_deriving.ord 187 + ppx_deriving.runtime 188 + ppx_deriving.show 189 + ppx_deriving.std 190 + ppx_enumerate 191 + ppx_enumerate.runtime-lib 192 + ppx_globalize 193 + ppx_hash 194 + ppx_hash.base_internalhash_types 195 + ppx_hash.expander 196 + ppx_hash.runtime-lib 197 + ppx_helpers 198 + ppx_helpers.modes_lib 199 + ppx_js_style 200 + ppx_sexp_conv 201 + ppx_sexp_conv.expander 202 + ppx_sexp_conv.runtime-lib 203 + ppx_shorthand 204 + ppx_template 205 + ppx_template.expander 206 + ppxlib 207 + ppxlib.ast 208 + ppxlib.astlib 209 + ppxlib.metaquot 210 + ppxlib.metaquot_lifters 211 + ppxlib.print_diff 212 + ppxlib.runner 213 + ppxlib.runner_as_ppx 214 + ppxlib.stdppx 215 + ppxlib.traverse 216 + ppxlib.traverse_builtins 217 + ppxlib_ast 218 + ppxlib_ast.ast 219 + ppxlib_ast.astlib 220 + ppxlib_ast.stdppx 221 + ppxlib_ast.traverse_builtins 222 + ppxlib_jane 223 + psq 224 + ptime 225 + ptime.clock 226 + ptime.clock.os 227 + ptime.top 228 + re 229 + re.emacs 230 + re.glob 231 + re.pcre 232 + re.perl 233 + re.posix 234 + re.str 235 + result 236 + rpclib 237 + rpclib-lwt 238 + rpclib.cmdliner 239 + rpclib.core 240 + rpclib.internals 241 + rpclib.json 242 + rpclib.markdown 243 + rpclib.xml 244 + rresult 245 + rresult.top 246 + runtime_events 247 + sedlex 248 + sedlex.ppx 249 + sedlex.utils 250 + seq 251 + sexp_type 252 + sexp_type.grammar 253 + sexplib0 254 + sha 255 + stdlib 256 + stdlib-shims 257 + stdlib_alpha 258 + stdlib_beta 259 + stdlib_stable 260 + stdlib_upstream_compatible 261 + stdune 262 + str 263 + stringext 264 + swhid_core 265 + thread-table 266 + threads 267 + threads.posix 268 + top-closure 269 + topkg 270 + tyxml 271 + tyxml.functor 272 + unix 273 + uri 274 + uri.services 275 + uri.services_full 276 + uring 277 + uucp 278 + uuseg 279 + uuseg.string 280 + uutf 281 + xdg 282 + xmlm 283 + yojson 284 + zarith_stubs_js
+11 -2
x-ocaml/src/jtw_client.ml
··· 23 23 let effective_url = 24 24 if is_cross_origin url then begin 25 25 (* Cross-origin workers are blocked by browsers. Work around this by 26 - creating a blob: URL that uses importScripts to load the real script. *) 27 - let js_code = Printf.sprintf {|importScripts("%s");|} url in 26 + creating a blob: URL that uses importScripts to load the real script. 27 + We also set __global_rel_url so the worker can resolve relative paths 28 + (e.g., lib/ocaml/stdlib.cmi) back to the correct origin. *) 29 + let base_dir = 30 + match String.rindex_opt url '/' with 31 + | Some i -> String.sub url 0 i 32 + | None -> url 33 + in 34 + let js_code = Printf.sprintf 35 + {|globalThis.__global_rel_url="%s";importScripts("%s");|} 36 + base_dir url in 28 37 let blob = Jv.new' (Jv.get Jv.global "Blob") 29 38 [| Jv.of_jv_array [| Jv.of_string js_code |]; 30 39 Jv.obj [| "type", Jv.of_string "application/javascript" |] |] in
+59
x-ocaml/test/package-lock.json
··· 1 + { 2 + "name": "x-ocaml-browser-tests", 3 + "version": "1.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "x-ocaml-browser-tests", 9 + "version": "1.0.0", 10 + "devDependencies": { 11 + "playwright": "^1.40.0" 12 + } 13 + }, 14 + "node_modules/fsevents": { 15 + "version": "2.3.2", 16 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 17 + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 18 + "dev": true, 19 + "hasInstallScript": true, 20 + "optional": true, 21 + "os": [ 22 + "darwin" 23 + ], 24 + "engines": { 25 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 26 + } 27 + }, 28 + "node_modules/playwright": { 29 + "version": "1.58.2", 30 + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", 31 + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", 32 + "dev": true, 33 + "dependencies": { 34 + "playwright-core": "1.58.2" 35 + }, 36 + "bin": { 37 + "playwright": "cli.js" 38 + }, 39 + "engines": { 40 + "node": ">=18" 41 + }, 42 + "optionalDependencies": { 43 + "fsevents": "2.3.2" 44 + } 45 + }, 46 + "node_modules/playwright-core": { 47 + "version": "1.58.2", 48 + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", 49 + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", 50 + "dev": true, 51 + "bin": { 52 + "playwright-core": "cli.js" 53 + }, 54 + "engines": { 55 + "node": ">=18" 56 + } 57 + } 58 + } 59 + }