Skip to contentSkip to Content
Feedback
Developer Guide

Developer Guide

How to build, test, and run everything in the repository from a checkout.

Folioh ships as one native Rust binary: folioh. The repository is one Cargo workspace plus the site template the binary renders through. This guide covers building the binary, running the tests, and working on the template.

Prerequisites

ToolNeeded forNotes
Rust toolchainThe workspaceInstall via rustup . rust-toolchain.toml pins the stable channel with clippy and rustfmt; rustup picks it up automatically.
Node.js 20.19+ and pnpm 10folioh build, folioh serve, and template workThe generated site renders through the bundled Nextra/Next.js template. Not needed to build the binary or run cargo test. Get pnpm with corepack enable pnpm && corepack prepare pnpm@10 --activate, or npm install -g pnpm@10.

Get the source

git clone https://github.com/pguijas/folioh cd folioh

Repository layout

The root Cargo.toml coordinates the packages in one virtual workspace.

  • crates/* contains the eleven engine crates: the CLI host (folioh-cli), configuration, parsers, page generation, plugin interfaces, site builder and watcher. folioh-cli is both the library and the package that builds the released folioh binary, from src/bin/folioh.rs.

Beside the crates:

  • docs/examples/ — the small example projects the guides build and the tests read. What a build must produce from them is checked in as a golden beside the crate that produces it, under crates/<crate>/tests/fixtures/.
  • template/ — the Nextra/Next.js site template, built with pnpm.
  • docs/ — the guides on this site. The root docs.yaml is the config of Folioh’s own site and theme/folioh-site its theme.
  • install.sh — the installer behind the install one-liner.

Build and verify

Run these from the repository root. The first three are the Rust checks CI runs (CI adds --locked); What CI runs lists the rest.

cargo fmt --all --check cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace cargo build --release
  • cargo fmt --all --check verifies formatting without rewriting files; drop --check to apply it.
  • cargo clippy --workspace --all-targets -- -D warnings lints every crate and target. CI treats warnings as errors with -D warnings; match it locally to avoid surprises.
  • cargo test --workspace runs the tests across all crates.
  • cargo build --release produces target/release/folioh using the thin-LTO, stripped release profile.

Installing the binary you built

The release build is a single self-contained executable. Copy target/release/folioh to a directory on your PATH (for example ~/.local/bin/) and run folioh --version to confirm the shell picks up your build rather than an installed release. To run a command without installing, cargo run --release -p folioh-cli -- build --clean builds and runs the same binary.

Tests

cargo test --workspace runs every crate’s tests. Unit tests live in a separate file beside the module they test: src/sidebar.rs declares #[cfg(test)] #[path = "sidebar_tests.rs"] mod tests; and loads src/sidebar_tests.rs. Tests for lib.rs, main.rs, or mod.rs use a tests.rs file in the same directory. This keeps access to private helpers without mixing the test bodies into implementation files, and a directory under src/ only ever holds submodules. Shared test-only helpers use separate child modules the same way.

Crate integration tests live under each crate’s tests/ directory. The CLI suites live in crates/folioh-cli/tests/, including the documentation checks in docs_surface.rs: that is the package that builds the binary, so they keep testing the actual executable.

The same directory holds the release surface checks (release_surface.rs), workspace layout rules (workspace_boundaries.rs), and the shared support the CLI suites load with mod common; (common/). cargo test -p folioh-cli runs all of these targets.

Narrow the run while iterating:

cargo test -p folioh-config # one crate cargo test -p folioh-cli --test release_surface # one integration test file cargo test -p folioh-cli the_one_liner # tests whose name contains a string

A new behaviour lands with its test: a unit test when it is local to one function, an integration test under the crate’s tests/ when it crosses crates or touches the filesystem, and a black-box run of the binary when it is what a user sees. The CLI tests in crates/folioh-cli/tests/ run the built binary through env!("CARGO_BIN_EXE_folioh") against a project from docs/examples/ and check the result against the checked-in golden. The fixtures and the tests are the definition of correct output.

Each crate keeps the fixtures its own tests read under crates/<crate>/tests/fixtures/: the config that must load without a warning (folioh-config), the sources each reader must parse with the IR they must parse to (golden_ir.json in folioh-lang-javascript and folioh-lang-rust; rich_package_ir.json and example_package_ir.json in folioh-lang-python, the second for the example package under docs/examples/generated-site/), the MDX contract baseline (folioh-plugins) and the pages, llms.txt and llms-full.txt a build must write (folioh-docs, compared again through the CLI by folioh-cli). FOLIOH_UPDATE_GOLDEN=1 rewrites the goldens of these tests:

FOLIOH_UPDATE_GOLDEN=1 cargo test -p folioh-lang-python --test golden FOLIOH_UPDATE_GOLDEN=1 cargo test -p folioh-lang-javascript --test golden FOLIOH_UPDATE_GOLDEN=1 cargo test -p folioh-lang-rust --test golden FOLIOH_UPDATE_GOLDEN=1 cargo test -p folioh-docs --test example_site FOLIOH_UPDATE_GOLDEN=1 cargo test -p folioh-plugins --test template_pins

The diff is the point: a golden regenerated without reading it turns a regression into the contract.

Benchmarks

crates/folioh-lang-python/benches/parse.rs is a criterion parse-throughput benchmark over the *.py files under $FOLIOH_BENCH_CORPUS. The crate declares it as a [[bench]] target with harness = false and keeps criterion as a dev-dependency, so cargo bench -p folioh-lang-python runs it.

The template

Everything folioh build renders comes from template/, a Next.js/Nextra workspace. Work on it directly with pnpm:

cd template pnpm install pnpm run dev

pnpm run lint and pnpm run typecheck are the template’s own checks.

What CI runs

One workflow gates changes: rust (.github/workflows/rust.yml). It runs on pushes and pull requests to main and develop and on v* tags, and the release workflow calls it before building. Its jobs install the stable toolchain and cache the build:

  • lint (Linux) runs cargo fmt --all --check and cargo clippy --workspace --all-targets --locked -- -D warnings, then the template’s pnpm install --frozen-lockfile, pnpm lint and pnpm typecheck on Node 22 and pnpm 10.
  • test (Linux and macOS) runs cargo test --workspace --locked on Node 22.
  • windows runs cargo test -p folioh-cli --test cli_surface --locked.
  • real-export (Linux) runs the two suites ignored by default because they run a real pnpm install and Next export: cargo test -p folioh-site --test e2e_export --locked -- --ignored and cargo test -p folioh-cli --test build_pipeline --locked -- --ignored.

CI does not run cargo build --release; the release workflow does.

Releasing

A release is a tag. Bump version in the root Cargo.toml and project.version in docs.yaml to the same string, spelled the Cargo way (0.3.0-a1; the tests check they agree), record the changes in CHANGELOG.md under a ## <version> — <date> heading, merge, then tag v<version> and push the tag:

git tag v0.3.0-a1 git push origin v0.3.0-a1

.github/workflows/release.yml fails before building if the tag does not match the workspace version. It then builds one archive per target on five targets (Linux and macOS on x86_64 and aarch64 as .tar.gz with the folioh binary at the archive root, which install.sh installs, and Windows x86_64 as a .zip to download by hand), runs install.sh against the two archives the runners execute natively (x86_64 Linux, arm64 macOS), and publishes a GitHub Release with the archives, a SHA256SUMS file and the version’s CHANGELOG.md entry as its notes; a version with no entry fails the release. install.sh downloads from releases/latest, so releases are published as regular releases, not prereleases, while the version string carries the alpha.

Pushing a tag is the publication decision; only the repository owner tags. Pull requests that touch the workflow or the installer run the build matrix without publishing, so the release path is proven before a tag exists.

Next steps