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:
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:
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
BTreeMapfor 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:
To reproduce the exact artifact that CI and Cloudflare Pages deploy (editable
easeo, regenerated llms-full.txt, then the build), use the build script:
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¶
- Create a directory under
integrations/withindex.js,index.d.ts, andpackage.json. - Support both default and named exports.
- Call
@easeo/corefor all payload building. Never re-implement logic. - Add a test under
tests/javascript/. - Add a page under
docs/integrations/and a nav entry inzensical.toml.