Skip to content

Contributing

Repository layout

easeo/
├── Cargo.toml                 # Rust workspace
├── pyproject.toml             # maturin / Python package
├── crates/
│   ├── easeo-core/            # all logic
│   ├── easeo-python/          # PyO3 bindings
│   └── easeo-node/            # napi-rs bindings
├── packages/core/             # @easeo/core wrapper
├── integrations/              # JS framework integrations
├── python/easeo/              # Python package, adapters, contrib
├── tests/                     # Python, JavaScript, conformance
├── fixtures/                  # shared fixtures
├── schemas/                   # JSON schemas
└── docs/                      # this documentation
    ├── overrides/             # Zensical theme overrides (main.html, partials)
    └── stylesheets/extra.css  # theme CSS (accent, mobile drawer, tab dropdowns)

Build and test

# Rust
cargo test --workspace
cargo fmt --all -- --check
cargo clippy --workspace -- -D warnings

# Python
maturin develop -m crates/easeo-python/Cargo.toml
pytest tests/python/

# JavaScript
cd packages/core
napi build --platform --release --manifest-path ../../crates/easeo-node/Cargo.toml
cp ../../crates/easeo-node/*.node .
cd ../..
node --test tests/javascript/*.cjs

# Cross-language conformance
python tests/conformance/test_conformance.py

Type definitions

Python type stubs for the native extension are generated and committed. PyO3 embeds introspection data for exported classes and functions; the exception types are appended by the script because they are created dynamically:

bash scripts/generate_stubs.sh   # writes python/easeo/_easeo_native.pyi

CI regenerates the stub and fails when the committed file is out of date.

The JavaScript native loader and its declarations are generated by @napi-rs/cli into packages/core/native.js and packages/core/native.d.ts:

bash scripts/generate_node_bindings.sh

packages/core/index.js is a hand-written wrapper that requires the generated loader, and packages/core/index.d.ts declares the public wrapper surface. It re-exports the generated record types under their public names (NodeSeoConfig as SEOConfig, and so on) and augments the config with the JavaScript extension points. When the Rust type names change, update those aliases. tests/javascript/types-consumer.ts is type checked with tsc --noEmit in CI.

Rules of the codebase

  • All logic lives in Rust. The Python and JavaScript packages are bindings and thin ergonomics wrappers. Do not duplicate resolution logic in a binding.
  • Determinism is non-negotiable. No timestamps, randomness, environment reads, or unordered maps in output. Use BTreeMap for anything that serializes.
  • Cross-language parity. A change to the Python API needs the JavaScript equivalent, and a conformance test where output could differ.
  • Escaping happens in the core. HTML and JSON-LD escaping is centralized so every binding is safe.

Documentation

Docs live in docs/ and build with Zensical. The site uses the easeo easeo.contrib.zensical extension to generate per-page SEO tags, so easeo must be importable by the same interpreter that runs Zensical.

uv handles this in one command. It builds the Rust extension from crates/easeo-python/ into a local .venv and runs Zensical with the dev dependency group, which includes zensical and markdown:

uv run zensical serve     # live preview on http://localhost:8000
uv run zensical build     # writes site/
uv run python scripts/generate_llms_full.py   # regenerate docs/llms-full.txt

If you prefer a manual environment, install the extra and run Zensical directly:

pip install -e ".[zensical]"
zensical serve

To reproduce the exact artifact that CI and Cloudflare Pages deploy (editable easeo, regenerated llms-full.txt, then the build), use the build script:

bash scripts/build_docs.sh

See Deploying the Docs for the Cloudflare Pages and GitHub Pages settings.

Keep prose free of em dashes and double-hyphen separators; use commas, colons, parentheses, or semicolons.

Adding a framework integration

  1. Create a directory under integrations/ with index.js, index.d.ts, and package.json.
  2. Support both default and named exports.
  3. Call @easeo/core for all payload building. Never re-implement logic.
  4. Add a test under tests/javascript/.
  5. Add a page under docs/integrations/ and a nav entry in zensical.toml.