> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reasonos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Web Apps

> Build web frontends with the web-vite rules — a dev server with hot reload, production bundles, component tests with coverage gates, and module federation

The `web-vite` rules give you a Vite-style development experience — dev server, hot
reload, TypeScript/JSX/CSS out of the box — built entirely on rbs's hermetic toolchains.
There is no Vite CLI or Webpack underneath: rbs bundles with Rollup (or optionally
Rolldown), and uses the **same bundler for development and production**, so what works in
dev works in prod.

| Rule                    | Purpose                                                |
| ----------------------- | ------------------------------------------------------ |
| `vite_app`              | A web application — dev server or production bundle    |
| `vite_dev_server`       | A standalone dev-server-only target                    |
| `vite_library`          | A shared component library, imported as `@scope/scope` |
| `vitest_test`           | Component tests with coverage gates                    |
| `mf_host` / `mf_remote` | Module-federation shell and micro-frontends            |
| `mf_ssr_custom`         | Server-side rendering with your own server             |

## Workspace setup

Declare the web toolchain once in `WORKSPACE.rbs`. One call sets up Node.js, esbuild, and
the bundler; add `nodejs_repository` entries for your app's packages and
`vitest_test_deps()` for testing:

```python theme={null}
load("@rbs//web-vite/toolchain.rbs", "web_js_bundle_toolchain")
load("@rbs//nodejs/dependencies.rbs", "nodejs_repository")
load("@rbs//web-vite/vitest.rbs", "vitest_test_deps")

web_js_bundle_toolchain(
    name = "web",
    nodejs_version = "22.15.1",
    esbuild_version = "0.24.0",
    rollup_version = "4.9.0",
    # Optional CSS tooling — set a version string to enable:
    # tailwind_version = "3.4.1",
    # sass_version = "1.71.0",
)

nodejs_repository(name = "react_repo", package = "react", version = "19.0.0")
nodejs_repository(name = "react_dom_repo", package = "react-dom", version = "19.0.0")
nodejs_repository(name = "types_react_repo", package = "@types/react", version = "19.0.0")

vitest_test_deps()
```

Dependencies work like `package.json`, split into three buckets on every app rule:

* `deps` — runtime dependencies, bundled for the browser
* `dev_deps` — build-time only (TypeScript, `@types/*`)
* `server_deps` — server-only (Express, ws), never bundled for the browser

See [Node.js & TypeScript](/languages/javascript#declare-npm-dependencies) for how npm
resolution and the `rbs.lock` lockfile work.

## Your first app

A minimal React app is a package with an `index.html`, sources under `src/`, and one
target per mode in `BUILD.rbs`:

```python theme={null}
load("@rbs//web-vite/core.rbs", "vite_app")
load("@rbs//web-vite/config.rbs", "define_config")

_DEPS = [
    "@external://react:19.0.0",
    "@external://react-dom:19.0.0",
]

# Development target — dev server with hot reload
vite_app(
    name = "dev",
    mode = "development",
    entry_point = "src/main.tsx",
    deps = _DEPS,
    dev_deps = ["@external://typescript:5.3.3"],
    config = define_config(port = 3000),
)

# Production target — optimized bundle + preview server
vite_app(
    name = "app",
    mode = "production",
    entry_point = "src/main.tsx",
    deps = _DEPS,
    dev_deps = ["@external://typescript:5.3.3"],
    port = 4173,
)
```

If `srcs` is omitted, sources are globbed from `src/` automatically (`.ts`, `.tsx`, `.js`,
`.jsx`, CSS/SCSS, JSON, SVG). Files in `public/` are copied into the output as-is.

## The dev server workflow

Run the development target and open the printed URL:

```bash theme={null}
rbs run :dev
```

The dev server:

* serves your app with **hot reload** — on every file change it rebuilds (unminified, with
  sourcemaps) and refreshes the browser
* uses the same bundler as the production build, so there are no dev-only surprises
* defaults to port `5173` with the reload channel on `24678`; set them via
  `define_config(port = ..., hmr_port = ...)`

You can also let rbs restart a target on changes from the outside:

```bash theme={null}
rbs run :dev --watch          # rebuild the target itself when files change
rbs run :shell :counter -p    # run several targets in parallel (Ctrl+C stops all)
```

## Production builds

`mode = "production"` (the default) produces an optimized bundle in `dist/` — minified,
tree-shaken, with content-hashed assets — and a preview server so you can run the built
app locally:

```bash theme={null}
rbs build :app     # produce the production bundle
rbs run :app       # build and serve the bundle (default port 4173, set with `port`)
```

Build behavior (target, minifier, sourcemaps, output directory, base path…) is tuned
through `define_config`:

```python theme={null}
config = define_config(
    target = "es2022",
    minify = "terser",        # "terser" | "esbuild" | False
    sourcemap = True,
    out_dir = "dist",
    base = "/",
    alias = {"@": "src"},
)
```

### vite\_app attributes

| Attribute             | Type        | Default          | Description                                                                                      |
| --------------------- | ----------- | ---------------- | ------------------------------------------------------------------------------------------------ |
| `srcs`                | label list  | glob of `src/**` | Source files                                                                                     |
| `deps`                | label list  | —                | Runtime dependencies (bundled for the browser)                                                   |
| `dev_deps`            | label list  | —                | Build-time dependencies (TypeScript, `@types/*`)                                                 |
| `server_deps`         | label list  | —                | Server-only dependencies (never bundled for the browser)                                         |
| `lib_deps`            | string dict | —                | Workspace libraries `{ import_scope: target_label }` — see [Shared libraries](#shared-libraries) |
| `entry_point`         | string      | `"src/main.tsx"` | Application entry point                                                                          |
| `index_html`          | string      | `"index.html"`   | HTML template                                                                                    |
| `mode`                | string      | `"production"`   | `development` (dev server) or `production` (bundle + preview)                                    |
| `port`                | int         | `4173`           | Preview server port for production targets                                                       |
| `config`              | dict        | —                | Configuration from `define_config()`                                                             |
| `css_config`          | dict        | —                | CSS options from `css_config()` — SCSS/SASS, PostCSS, Tailwind                                   |
| `node_compat`         | dict        | —                | Browser-compat options from `node_compat_config()` for Node-flavored packages                    |
| `tsconfig`            | string      | —                | Path to a `tsconfig.json` to extend                                                              |
| `ts_compiler_options` | string dict | —                | TypeScript compiler options                                                                      |
| `bundler`             | string      | `"rollup"`       | Bundler engine: `rollup` or `rolldown`                                                           |

`vite_dev_server` is a standalone rule with the same attributes (minus `mode`, `port`, and
`server_deps`) for when you want a dev-server-only target.

<Note>
  `rollup` is the default, battle-tested engine. `rolldown` (the Rust engine behind Vite 8)
  is available as an opt-in via `bundler = "rolldown"` and should be considered
  experimental.
</Note>

### CSS: SCSS, PostCSS, Tailwind

Plain CSS and CSS Modules (`*.module.css`) work with zero configuration. For
preprocessors and Tailwind, pass a `css_config()`:

```python theme={null}
load("@rbs//web-vite/config.rbs", "css_config")

vite_app(
    name = "app",
    deps = _DEPS,
    css_config = css_config(
        preprocessor = "scss",   # "scss" | "sass"
        tailwind = True,         # auto-adds tailwindcss + autoprefixer
    ),
)
```

A `tailwind.config.js` (or `.ts`) and `postcss.config.js` in the package are picked up
automatically. Remember to enable the matching tool versions in
`web_js_bundle_toolchain` (`tailwind_version`, `sass_version`, …).

## Shared libraries

`vite_library` shares components between apps at the source level — the consuming app
compiles the library's TS/TSX together with its own code, so there is no separate library
build step and tree shaking sees the whole graph.

```python theme={null}
# client/libs/ui/BUILD.rbs
load("@rbs//web-vite/core.rbs", "vite_library")

vite_library(
    name = "ui",
    srcs = glob(["src/**/*.ts", "src/**/*.tsx", "src/**/*.css"]),
    entry_point = "src/index.ts",
    deps = ["@external://react:19.0.0"],   # forwarded to consumers automatically
)
```

Consumers declare the library under an import scope and import it like an npm package:

```python theme={null}
vite_app(
    name = "app",
    deps = _DEPS,
    lib_deps = {"@acme/ui": "//client/libs/ui:ui"},
)
```

```tsx theme={null}
import { Button } from "@acme/ui";
import { Modal } from "@acme/ui/components/Modal";   // subpath imports work too
```

Any npm packages the library declares in its `deps` are forwarded to the consuming app —
you don't repeat them.

| Attribute     | Type        | Default                           | Description                              |
| ------------- | ----------- | --------------------------------- | ---------------------------------------- |
| `srcs`        | label list  | glob of `src/**`                  | Library source files                     |
| `deps`        | label list  | —                                 | npm dependencies, forwarded to consumers |
| `dev_deps`    | label list  | —                                 | Build-time dependencies                  |
| `lib_deps`    | string dict | —                                 | Nested library dependencies              |
| `entry_point` | string      | auto-detected (`src/index.tsx` …) | Library entry point                      |
| `tsconfig`    | string      | —                                 | Path to a `tsconfig.json`                |

## Testing and coverage

Component tests use `vitest_test` — real Vitest in a jsdom environment with V8 lcov
coverage:

```python theme={null}
load("@rbs//web-vite/vitest.rbs", "vitest_test")

vitest_test(
    name = "test",
    srcs = [
        "src/Counter.tsx",
        "src/Counter.test.tsx",
    ],
    deps = [
        "@external://react:19.0.0",
        "@external://react-dom:19.0.0",
        "@external://@testing-library/react:16.3.0",
        "@external://@testing-library/dom:10.4.0",
    ],
    min_line_coverage = 80,
)
```

```bash theme={null}
rbs test :test        # run the suite
rbs coverage :test    # collect coverage and FAIL below the thresholds
```

Set `min_line_coverage` / `min_branch_coverage` / `min_function_coverage` on the target to
gate coverage in CI. Tests that exercise a `vite_library` declare it via the same
`lib_deps` scopes as the app. Full attribute reference and setup:
[Component testing with vitest\_test](/languages/javascript#component-testing-with-vitest_test).

## Micro-frontends with Module Federation

The `mf_host` and `mf_remote` rules build micro-frontend architectures on the official
`@module-federation/enhanced` runtime — with runtime version negotiation, shared-dependency
singletons, and interoperability with Webpack/Rspack/Vite federation apps. Both support
development and production modes, using the same bundler pipeline in each.

A **remote** exposes modules; a **host** (shell) loads them by URL:

```python theme={null}
# remote/BUILD.rbs — a micro-frontend exposing a module
load("@rbs//web-vite/federation.rbs", "mf_remote")

mf_remote(
    name = "counter",
    mf_name = "counter",                       # the name hosts use to reference it
    entry_point = "src/main.tsx",
    deps = [
        "@external://react:19.0.0",
        "@external://react-dom:19.0.0",
    ],
    exposes = {
        "./Counter": "src/Counter.tsx",
    },
    shared = {"react": "19.0.0", "react-dom": "19.0.0"},
    port = 3001,
)
```

```python theme={null}
# shell/BUILD.rbs — the host application
load("@rbs//web-vite/federation.rbs", "mf_host")

mf_host(
    name = "shell",
    entry_point = "src/main.tsx",
    deps = [
        "@external://react:19.0.0",
        "@external://react-dom:19.0.0",
    ],
    remotes = {
        "counter": "http://localhost:3001",
    },
    shared = {"react": "19.0.0", "react-dom": "19.0.0"},
    port = 3000,
)
```

Run the whole system in one command:

```bash theme={null}
rbs run shell:shell remote:counter --parallel
```

`shared` declares the packages every micro-frontend must resolve to a single instance
(React being the classic case) — each entry becomes a singleton with a
`^version` requirement, enforced by the federation runtime at load time.

For production targets, pass `options = build_options(mode = "prod", minify = True)`
(loaded from `@rbs//web-vite/federation.rbs`).

### mf\_host attributes

| Attribute                          | Type          | Default          | Description                                                           |
| ---------------------------------- | ------------- | ---------------- | --------------------------------------------------------------------- |
| `srcs`                             | label list    | glob of `src/**` | Source files                                                          |
| `deps`                             | label list    | —                | Browser-safe runtime dependencies                                     |
| `dev_deps` / `server_deps`         | label list    | —                | Build-time / server-only dependencies                                 |
| `lib_deps`                         | string dict   | —                | Workspace libraries `{ import_scope: target_label }`                  |
| `lib_dep_targets`                  | label list    | —                | The `lib_deps` labels repeated as labels, to establish build ordering |
| `entry_point`                      | string        | `"src/main.tsx"` | Entry point                                                           |
| `remotes`                          | string dict   | —                | Remotes to load: `{ name: url }`                                      |
| `remote_exposes`                   | dict          | —                | Exposed module names per remote: `{ remote: [names] }`                |
| `shared`                           | string dict   | —                | Singleton shared dependencies `{ package: version }`                  |
| `host`                             | string        | `"localhost"`    | Server bind host                                                      |
| `port`                             | int           | `3000`           | Server port                                                           |
| `hmr_port`                         | int           | `24680`          | Hot-reload channel port                                               |
| `options`                          | dict          | dev defaults     | Build options from `build_options()`                                  |
| `framework_config`                 | dict          | React            | Framework config, e.g. `react_config(version = "19.0.0")`             |
| `css_config`                       | dict          | —                | CSS options from `css_config()`                                       |
| `env`                              | string dict   | —                | Environment variables                                                 |
| `tsconfig` / `ts_compiler_options` | string / dict | —                | TypeScript configuration                                              |
| `bundler`                          | string        | `"rollup"`       | `rollup` or `rolldown`                                                |

### mf\_remote attributes

`mf_remote` shares the attributes above (defaults: `port = 3001`, `hmr_port = 24681`),
minus `remotes`, plus:

| Attribute  | Type        | Default                   | Description                                         |
| ---------- | ----------- | ------------------------- | --------------------------------------------------- |
| `mf_name`  | string      | target name               | The federation name hosts reference this remote by  |
| `exposes`  | string dict | —                         | Exposed modules: `{ "./Name": "src/path.tsx" }`     |
| `host_url` | string      | `"http://localhost:3000"` | Host app URL, used for shared-singleton enforcement |

### Server-side rendering

`mf_ssr_custom` builds a federation-aware SSR app where **you** own the server: it bundles
a client build and a server build from your own server entry (e.g. an Express or NestJS
app that renders React on the server), and wires up dev mode with hot reload.

```python theme={null}
load("@rbs//web-vite/ssr.rbs", "mf_ssr_custom")

mf_ssr_custom(
    name = "app",
    entry_point = "src/main.tsx",
    srcs = glob(["src/**/*.tsx", "src/**/*.ts", "src/**/*.css"]),
    server_entry = "server/main.server.ts",
    server_srcs = glob(["server/**/*.ts", "server/**/*.tsx"]),
    deps = _DEPS,                    # browser-safe (client + server shared)
    server_deps = [                  # server-only, never sent to the browser
        "@external://isbot:5.1.0",
    ],
    dev_deps = ["@external://typescript:5.3.3"],
    shared = {"react": "19.0.0", "react-dom": "19.0.0"},
    port = 3000,
)
```

It accepts the federation attributes (`remotes`, `shared`, `options`, `css_config`, …)
plus `server_entry`, `server_srcs`, `server_tsconfig`, `server_externals` (packages kept
external in the server bundle), and `static_files_path`.

## TypeScript configuration

The web-vite rules **generate and own** the TypeScript configuration at every level:

* Each build writes a self-contained `tsconfig` into the staged build tree, configured for
  the bundler (`moduleResolution: "bundler"`, `jsx: "react-jsx"`, path mappings for your
  `lib_deps` scopes).
* Builds also generate an editor config (under `.rbs/lsp/`) so the IDE resolves npm
  packages, JSX, and `@scope/scope` library imports on a cold checkout — no build-first,
  no manual setup.

To adjust compiler behavior, use the target's attributes:

```python theme={null}
vite_app(
    name = "app",
    deps = _DEPS,
    ts_compiler_options = {"target": "ES2022"},   # override individual options
    tsconfig = "tsconfig.json",                   # or extend your own base config
)
```

<Warning>
  Don't hand-maintain a parallel `tsconfig.json` that contradicts the generated
  configuration (e.g. different `jsx` or `moduleResolution` settings) and expect it to drive
  the build — the generated configs win, and fighting them typically shows up as phantom
  JSX or import errors in the editor. Express customizations through `ts_compiler_options`
  or the `tsconfig` attribute so the generated configs incorporate them.
</Warning>
