Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

# Build the publishable packages (tokens → xote → skill).
- name: Build packages
run: npm run build:packages
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

# Build the packages the website consumes (tokens → xote), then the site.
- name: Build packages
run: npm run build:packages
Expand Down
4 changes: 1 addition & 3 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions website/reativa/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# reativa clone + Melange build output, produced by `npm run reativa`.
.build/
_build/
36 changes: 23 additions & 13 deletions website/reativa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<div>` that Xote renders.

Expand All @@ -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.
17 changes: 0 additions & 17 deletions website/reativa/dune-project

This file was deleted.

82 changes: 82 additions & 0 deletions website/scripts/build-reativa.mjs
Original file line number Diff line number Diff line change
@@ -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 <target>/<emit-dir>/<module>.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}`);
5 changes: 3 additions & 2 deletions website/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading