From 42eb49db4e3314b2c8ee5ad946381e4816a31db3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 18:50:28 +0000 Subject: [PATCH 1/2] ci: compile the reativa (Melange) bundle in CI and for Pages deploy Add an OCaml/Melange setup to both workflows so the "Reativa" tab ships the real compiled bundle instead of the checked-in placeholder: - ci.yml: sets up OCaml 5.2, installs dune/melange/mlx, pins reativa, and runs `npm run reativa` so the .mlx examples are compiled and type-checked on every PR. - deploy-pages.yml: same, before the website build, so the published site renders the live reativa components. The Melange build stays out of the local `npm run build` (opam/melange aren't needed there); CI regenerates src/reativa.bundle.js (built = true) ahead of the Vite build, which scans it for Tailwind classes. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Rqv9Uid5eY56AYzXPphg1c --- .github/workflows/ci.yml | 17 +++++++++++++++++ .github/workflows/deploy-pages.yml | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09c8a61..39e491b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,23 @@ jobs: - name: Install run: npm ci + # OCaml toolchain for the reativa (Melange) examples, so the "Reativa" + # tab's bundle is compiled and type-checked in CI (not needed for the + # local website build, which uses a checked-in placeholder). + - name: Set up OCaml + uses: ocaml/setup-ocaml@v3 + with: + ocaml-compiler: "5.2" + dune-cache: true + + - name: Install Melange toolchain + reativa + run: | + opam install -y dune melange mlx + opam pin add -y reativa https://github.com/brnrdog/reativa.git + + - name: Build reativa bundle (OCaml → esbuild) + run: npm run reativa --workspace xpecs-website + # Build the publishable packages (tokens → xote → skill). - name: Build packages run: npm run build:packages diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 1bb76fa..1676d19 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -39,6 +39,24 @@ jobs: - name: Install run: npm ci + # OCaml toolchain for the reativa (Melange) examples rendered in the + # website's "Reativa" tab. Kept out of the local build (opam/melange + # aren't needed there), but compiled here so the deployed site ships the + # real bundle instead of the checked-in placeholder. + - name: Set up OCaml + uses: ocaml/setup-ocaml@v3 + with: + ocaml-compiler: "5.2" + dune-cache: true + + - name: Install Melange toolchain + reativa + run: | + opam install -y dune melange mlx + opam pin add -y reativa https://github.com/brnrdog/reativa.git + + - name: Build reativa bundle (OCaml → esbuild) + run: npm run reativa --workspace xpecs-website + # Build the packages the website consumes (tokens → xote), then the site. - name: Build packages run: npm run build:packages From 64d2d2e92719a4285fd74eaa1302ff9eb364ef38 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 19:00:52 +0000 Subject: [PATCH 2/2] Fix reativa build: compile examples inside a reativa checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI surfaced `Error: Library "reativa" not found`: reativa's core library has no public_name, so it's a private dune library that can't be consumed as an installed opam package (only its own in-repo demo can use it). Consuming it via `opam pin` + `(libraries reativa)` never works. Build the examples the same way reativa's demo does — inside the reativa dune project. website/scripts/build-reativa.mjs clones reativa (pinned commit), drops the example dune + xpecs_reativa.mlx into a subdirectory of it (so the private `reativa` library and reativa.mlx_ppx resolve), runs dune build @melange, and bundles the emitted entry to src/reativa.bundle.js with esbuild. - Remove the standalone website/reativa/dune-project (reativa's own is used). - Replace the reativa:build/reativa:bundle npm scripts with the clone-based build-reativa.mjs, invoked by `npm run reativa`. - Drop `opam pin add reativa` from both workflows (private lib; the script clones it); keep the dune/melange/mlx install. - Scope the Tailwind @source for reativa sources to the top-level .mlx so it doesn't recurse into the .build/ clone. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Rqv9Uid5eY56AYzXPphg1c --- .github/workflows/ci.yml | 8 +-- .github/workflows/deploy-pages.yml | 8 +-- website/package.json | 4 +- website/reativa/.gitignore | 2 + website/reativa/README.md | 36 ++++++++----- website/reativa/dune-project | 17 ------- website/scripts/build-reativa.mjs | 82 ++++++++++++++++++++++++++++++ website/src/styles.css | 5 +- 8 files changed, 119 insertions(+), 43 deletions(-) delete mode 100644 website/reativa/dune-project create mode 100644 website/scripts/build-reativa.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39e491b..9d154d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,11 +28,11 @@ jobs: ocaml-compiler: "5.2" dune-cache: true - - name: Install Melange toolchain + reativa - run: | - opam install -y dune melange mlx - opam pin add -y reativa https://github.com/brnrdog/reativa.git + - name: Install Melange toolchain + run: opam install -y dune melange mlx + # Clones reativa (pinned), compiles the .mlx examples inside its dune + # project, and bundles the output to website/src/reativa.bundle.js. - name: Build reativa bundle (OCaml → esbuild) run: npm run reativa --workspace xpecs-website diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 1676d19..7eff2a2 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -49,11 +49,11 @@ jobs: ocaml-compiler: "5.2" dune-cache: true - - name: Install Melange toolchain + reativa - run: | - opam install -y dune melange mlx - opam pin add -y reativa https://github.com/brnrdog/reativa.git + - name: Install Melange toolchain + run: opam install -y dune melange mlx + # Clones reativa (pinned), compiles the .mlx examples inside its dune + # project, and bundles the output to website/src/reativa.bundle.js. - name: Build reativa bundle (OCaml → esbuild) run: npm run reativa --workspace xpecs-website diff --git a/website/package.json b/website/package.json index b4ed631..10038be 100644 --- a/website/package.json +++ b/website/package.json @@ -22,9 +22,7 @@ "res:build": "rescript", "res:clean": "rescript clean", "res:dev": "rescript -w", - "reativa:build": "cd reativa && opam exec -- dune build @melange", - "reativa:bundle": "esbuild reativa/_build/default/output/xpecs_reativa.js --bundle --format=esm --outfile=src/reativa.bundle.js", - "reativa": "npm run reativa:build && npm run reativa:bundle", + "reativa": "node scripts/build-reativa.mjs", "dev": "npm run gen && npm run res:build && vite", "build": "npm run gen && npm run res:build && npm run checks && vite build", "preview": "vite preview", diff --git a/website/reativa/.gitignore b/website/reativa/.gitignore index 69fa449..b474270 100644 --- a/website/reativa/.gitignore +++ b/website/reativa/.gitignore @@ -1 +1,3 @@ +# reativa clone + Melange build output, produced by `npm run reativa`. +.build/ _build/ diff --git a/website/reativa/README.md b/website/reativa/README.md index 6f1bfe5..2481df3 100644 --- a/website/reativa/README.md +++ b/website/reativa/README.md @@ -24,15 +24,23 @@ once and only the reactive regions update in place. ## How it plugs into the (ReScript/Vite) website -This is a **self-contained Melange workspace**, deliberately separate from the -ReScript/Vite build so the website keeps compiling without an OCaml toolchain: - -1. `dune build @melange` compiles `xpecs_reativa.mlx` to ES modules under - `_build/default/output/`. -2. `esbuild` bundles the emitted entry into +The Melange build is deliberately separate from the ReScript/Vite build, so the +website keeps compiling without an OCaml toolchain. +[`../scripts/build-reativa.mjs`](../scripts/build-reativa.mjs) (run by +`npm run reativa`) does the work: + +1. **Clones reativa** (pinned to a commit) into `.build/reativa/`. reativa's core + library has no `public_name`, so it is a *private* dune library that can't be + consumed as an installed opam package — its own demo builds only because it + lives inside the reativa dune project. So we do the same: it copies + [`dune`](dune) + [`xpecs_reativa.mlx`](xpecs_reativa.mlx) into a subdirectory + of the clone, where the private `reativa` library and the `reativa.mlx_ppx` + ppx are in scope. +2. `dune build @melange` compiles them to ES modules. +3. `esbuild` bundles the emitted entry into [`../src/reativa.bundle.js`](../src/reativa.bundle.js) — a single ES module exporting `mount_example(specId, containerId)`, `example_ids`, and `built`. -3. The website ([`../src/ReativaExamples.res`](../src/ReativaExamples.res)) +4. The website ([`../src/ReativaExamples.res`](../src/ReativaExamples.res)) imports that bundle and, when the **Reativa** tab is opened, imperatively mounts the example into a container `
` that Xote renders. @@ -41,23 +49,25 @@ so the site compiles out of the box and the Reativa tab shows a build hint. ## Building -Requires the OCaml toolchain. One-time setup: +Requires the OCaml toolchain (an opam switch on OCaml **5.1+**, since reativa +needs `melange >= 3`). One-time setup: ```bash -opam switch create . 5.2.1 --deps-only # or use an existing 5.1+ switch +opam switch create . 5.2.1 # or reuse an existing 5.1+ switch opam install dune melange mlx -opam pin add reativa https://github.com/brnrdog/reativa.git ``` Then, from the `website/` directory: ```bash -npm run reativa # dune build @melange + esbuild bundle +npm run reativa # clone reativa → dune build @melange → esbuild bundle npm run dev # start the site; open any of the specs above → "Reativa" ``` `npm run reativa` overwrites `../src/reativa.bundle.js` with the real compiled -output (`built = true`). Re-run it after editing `xpecs_reativa.mlx`. +output (`built = true`). Re-run it after editing `xpecs_reativa.mlx`. To move to +a newer reativa, bump `REATIVA_REF` in `build-reativa.mjs`. > Note: `npm run reativa` is intentionally **not** part of `npm run build`/`dev` -> or CI — it needs opam/melange, which the rest of the website build does not. +> (it needs opam/melange, which the rest of the website build does not) — but CI +> runs it, so PRs compile the examples and Pages ships the real bundle. diff --git a/website/reativa/dune-project b/website/reativa/dune-project deleted file mode 100644 index 4919557..0000000 --- a/website/reativa/dune-project +++ /dev/null @@ -1,17 +0,0 @@ -(lang dune 3.16) - -; A self-contained Melange workspace that compiles the reativa (OCaml) Xpecs -; examples to ES modules. It is intentionally separate from the ReScript/Vite -; website build: `npm run reativa` (in website/) builds this and bundles the -; output into website/src/reativa.bundle.js. Requires opam + melange; see -; website/reativa/README.md. - -(using melange 0.1) - -(dialect - (name mlx) - (implementation - (extension mlx) - (merlin_reader mlx) - (preprocess - (run mlx-pp %{input-file})))) diff --git a/website/scripts/build-reativa.mjs b/website/scripts/build-reativa.mjs new file mode 100644 index 0000000..4c562f4 --- /dev/null +++ b/website/scripts/build-reativa.mjs @@ -0,0 +1,82 @@ +// Build the reativa (OCaml + Melange) example bundle consumed by the website's +// "Reativa" tab (see website/reativa/README.md). +// +// reativa's core library is a *private* dune library (no public_name), so it +// can't be consumed as an installed opam library — its own demo works only +// because it lives inside the reativa dune project. We do the same: clone +// reativa (pinned), drop website/reativa/{dune,xpecs_reativa.mlx} into a +// subdirectory of that project so the private `reativa` library and the +// `reativa.mlx_ppx` ppx resolve, compile to JS with Melange, then bundle the +// emitted entry into website/src/reativa.bundle.js with esbuild. +// +// Requires: opam switch with `dune`, `melange`, `mlx` installed, plus git and +// the website's esbuild devDependency. Not part of `npm run build`/CI's website +// build — invoked explicitly by `npm run reativa`. + +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, copyFileSync, readdirSync } from "node:fs"; +import { resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REATIVA_REPO = "https://github.com/brnrdog/reativa.git"; +// Pin a known-good commit so the build is reproducible. Bump deliberately. +const REATIVA_REF = "c94697df4bcd16fae59f900a01f7c60964606492"; + +const websiteDir = resolve(fileURLToPath(import.meta.url), "../.."); +const reativaDir = resolve(websiteDir, "reativa"); +const buildDir = resolve(reativaDir, ".build"); +const clone = resolve(buildDir, "reativa"); +const exDir = resolve(clone, "xpecs_examples"); + +const sh = (file, args, cwd) => + execFileSync(file, args, { cwd, stdio: "inherit" }); + +// 1. Clone reativa (or reuse a previous clone) and check out the pinned commit. +mkdirSync(buildDir, { recursive: true }); +if (!existsSync(resolve(clone, ".git"))) { + sh("git", ["clone", REATIVA_REPO, "reativa"], buildDir); +} +sh("git", ["fetch", "--quiet", "origin"], clone); +sh("git", ["checkout", "--quiet", REATIVA_REF], clone); + +// 2. Drop the example dune + source into the reativa project (same dune-project +// ⇒ the private `reativa` library and reativa.mlx_ppx are in scope). +mkdirSync(exDir, { recursive: true }); +copyFileSync(resolve(reativaDir, "dune"), resolve(exDir, "dune")); +copyFileSync( + resolve(reativaDir, "xpecs_reativa.mlx"), + resolve(exDir, "xpecs_reativa.mlx"), +); + +// 3. Compile the examples (and reativa itself) to ES modules via Melange. +sh("opam", ["exec", "--", "dune", "build", "@melange"], clone); + +// 4. Bundle the emitted entry into a single ES module under the website's src. +// melange.emit mirrors the source path under the target dir, so the entry is +// normally //.js — but locate it defensively in +// case the nesting differs across dune/melange versions. +const outputRoot = resolve(clone, "_build/default/xpecs_examples/output"); +const findEntry = (dir) => { + for (const name of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, name.name); + if (name.isDirectory()) { + const hit = findEntry(p); + if (hit) return hit; + } else if (name.name === "xpecs_reativa.js") { + return p; + } + } + return null; +}; +const entry = findEntry(outputRoot); +if (!entry) { + throw new Error(`could not find emitted xpecs_reativa.js under ${outputRoot}`); +} +const outfile = resolve(websiteDir, "src/reativa.bundle.js"); +sh( + "npx", + ["--yes", "esbuild", entry, "--bundle", "--format=esm", `--outfile=${outfile}`], + websiteDir, +); + +console.log(`\n✓ reativa bundle written to ${outfile}`); diff --git a/website/src/styles.css b/website/src/styles.css index e49e1ab..cf02170 100644 --- a/website/src/styles.css +++ b/website/src/styles.css @@ -16,8 +16,9 @@ * their Tailwind classes as string literals in this bundle — scan it too so * any class used only by a reativa example still generates a utility. */ @source "./reativa.bundle.js"; -/* And the reativa source, so utilities exist even before the bundle is built. */ -@source "../reativa/**/*.mlx"; +/* And the reativa source, so utilities exist even before the bundle is built. + * Top-level only — avoid recursing into the .build/ reativa clone. */ +@source "../reativa/*.mlx"; html { scroll-behavior: smooth;