haven 0.1.1

Actix + React + Vite integration for server-rendered applications
Documentation
# haven

`haven` is a Rust crate for building server-rendered React apps on top of `actix-web`.

It gives an Actix application a React rendering layer. Your Rust server stays in charge of HTTP,
routing, middleware, cookies, sessions, and responses, while Haven runs your React server entry
inside an embedded `deno_core` runtime and turns page renders into either full HTML responses or
JSON page envelopes for client-side visits.

A good mental model is:

- Actix owns the request.
- Haven renders the page.
- Vite builds the browser and server bundles.
- React owns the UI.

This repository contains the Rust crate, the browser package published as `@ovior/haven`, a small
starter template, and an example Actix server.

## Why Haven exists

Haven is meant for apps where Rust is already the backend, but you still want the ergonomics of
React pages, Vite, client-side navigation, SSR, redirects, partial reloads, and shared page data.

The crate keeps the integration point small. You create one shared `RendererState` when the server
starts, store it in Actix app data, and then use a request-scoped `Renderer` inside handlers.

That gives you a flow that feels natural in Actix:

```rust
async fn home(renderer: Renderer) -> HttpResponse {
    renderer
        .render("index")
        .props(json!({
            "message": "Hello from Actix"
        }))
        .await
}
```

The handler decides what page to render and which props to pass. Haven takes care of calling the
JavaScript server entry and formatting the response for either a normal browser load or an in-app
visit.

## Crate API

The main Rust types are:

- `RendererState`
- `Renderer`
- `RendererConfig`
- `Redirect`

`RendererState` owns the JavaScript runtime bridge. In a typical app, you build it once at startup
and share it through `web::Data`.

`Renderer` is request-scoped. It wraps the current Actix request and uses the shared
`RendererState` to render pages, stream pages, redirect, and inspect Haven-specific request
metadata.

```rust
use actix_web::{App, HttpResponse, HttpServer, web};
use haven::actix::Renderer;
use haven::{RendererConfig, RendererMode, RendererState};
use serde_json::json;

async fn home(renderer: Renderer) -> HttpResponse {
    renderer
        .render("index")
        .props(json!({
            "message": "Hello from Actix"
        }))
        .await
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let renderer_state = RendererState::from_config_sync(RendererConfig {
        mode: RendererMode::Development,
        ..RendererConfig::default()
    })
    .expect("renderer");

    let renderer_data = web::Data::new(renderer_state);

    HttpServer::new(move || {
        App::new()
            .app_data(renderer_data.clone())
            .route("/", web::get().to(home))
    })
    .bind(("127.0.0.1", 3000))?
    .run()
    .await
}
```

## Rendering from a request

Most handlers use one of these patterns:

```rust
renderer.render("page").await
renderer.render("page").props(data).await
renderer.stream("page").await
renderer.redirect("/other-page")
```

`Renderer` also exposes request-aware helpers:

```rust
renderer.uri()
renderer.headers()
renderer.is_visit()
renderer.partial_reload()
```

These are useful when a handler needs to adjust props based on the current request, detect a
client-side visit, or handle partial reload behavior.

## Rendering outside a request

For background jobs, tests, or places where you do not have an Actix request, use `RendererState`
directly:

```rust
let page = renderer_state
    .render("index")
    .url("/")
    .props(serde_json::json!({ "message": "hello" }))?
    .await?;
```

This path is useful when you want the same page rendering machinery without going through an
Actix handler.

## JavaScript app layout

The app source usually lives in `app/`.

The important files are:

- `app/entry-server.jsx` or `app/entry-server.tsx`
- `app/entry-client.jsx` or `app/entry-client.tsx`
- `app/renderer.jsx` or `app/renderer.tsx`
- `app/pages/**`

A minimal server entry looks like this:

```jsx
import { defineServerEntry } from "@ovior/haven/page";
import { renderEnvelope, streamEnvelope } from "./renderer";

export const { renderPage, streamPage } = defineServerEntry({
  renderEnvelope,
  streamEnvelope,
});
```

`defineServerEntry(...)` wires up the SSR globals used by Haven's embedded runtime and returns the
named exports expected by the Vite dev/runtime bridge.

Pages are referenced by string ids derived from the file layout under `app/pages`.

For example:

- `app/pages/index.jsx` becomes `"index"`
- `app/pages/about.jsx` becomes `"about"`
- `app/pages/admin/users.tsx` becomes `"admin/users"`

## Browser package

The browser helpers are published as `@ovior/haven`.

Public entry points include:

- `@ovior/haven/page`
- `@ovior/haven/data`
- `@ovior/haven/router`
- `@ovior/haven/framework`
- `@ovior/haven/runtime`
- `@ovior/haven/i18n`

These modules provide the client-side pieces for page envelopes, navigation, runtime data,
translations, and framework-level helpers.

## Starter app

The repository includes a CLI that can generate a local starter app from `templates/starter`.

Create a new app:

```bash
cargo run --bin haven -- new my-app
```

Then install JavaScript dependencies and run the development supervisor:

```bash
cd my-app
npm install
cargo run --bin haven -- dev
```

The generated app uses:

- `actix-web` for the Rust server
- `actix-files` for serving built client assets
- `@ovior/haven` for browser-side helpers
- Vite for client and server JavaScript builds

## Development mode

In development, Haven usually talks to a Haven-aware Vite dev server.

The key `RendererConfig` fields are:

- `mode: RendererMode::Development`
- `vite_dev_server_origin`
- `start_vite_dev_server`

When `vite_dev_server_origin` is set, Haven sends SSR requests through the dev runtime at that
origin. This keeps the development loop close to a normal Vite React app while still letting Actix
handle the HTTP request.

By default, Haven leaves process management to you. If you want the renderer to start the Vite dev
runtime itself, set:

```rust
start_vite_dev_server: true
```

For local apps, the bundled command is usually the easiest path:

```bash
cargo run --bin haven -- dev
```

That command starts both sides and restarts the Rust server when Rust files change.

## Production mode

In production, Haven reads the built server entry and client manifest from the Vite output.

`RendererConfig::default()` points at the conventional paths:

- server entry: `dist/server/entry-server.mjs`
- client manifest: `dist/client/.vite/manifest.json`
- app source: `app/`

A production build needs both pieces:

1. the Rust binary
2. the Vite client/server output

Build the JavaScript assets:

```bash
npm install
npm run build:js
```

Then build or run the Rust app as usual:

```bash
cargo build --release
```

## Browser protocol

Haven supports both full-page HTML responses and JSON page visits.

On a normal browser request, the server returns HTML.

On an in-app Haven navigation, the browser sends protocol headers that allow the server to return a
serialized page envelope instead. The client can then update the current page without doing a full
browser reload.

The same protocol supports:

- same-app redirects
- hard redirects
- validation errors on the page envelope
- partial reloads for selected top-level props or resources

This is the part that lets a Rust-backed React app feel like a modern client-side app while still
rendering through the server.

## Translations

Translations live under:

```text
locales/<locale>/*.json
```

Rust loads the dictionaries, and the embedded runtime exposes translation helpers to JavaScript.
That allows pages to translate during SSR without each page module carrying the full translation
payload.

## Example server

The repository includes an example Actix server:

```bash
cargo run --example actix_server
```

By default, the example expects a dev runtime at:

```text
http://127.0.0.1:5174
```

and binds the Rust app on port `4000`.

## Repository commands

Useful commands while working on this repository:

```bash
npm install
npm run build:public
npm run build:js
cargo check
cargo test
```

Browser tests use Playwright:

```bash
npm run test:browser
```

## Design notes

Haven is intentionally focused on one integration path: Actix plus React plus Vite.

That focus keeps the crate small and predictable. It also means a production app should be built
with the expected Vite output available, and page ids currently come from the file layout rather
than generated Rust types.

For apps that already use Actix as the server boundary, this keeps the moving parts easy to reason
about: Rust handles the web server, React handles the UI, and Haven connects the two at the page
rendering layer.

## License

MIT