> ## 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.

# Node.js & TypeScript

> Build, run, and test Node.js and TypeScript code with rbs — hermetic toolchains, built-in npm dependency management, and coverage-gated tests

rbs builds Node.js and TypeScript projects without requiring Node, npm, or yarn on your
machine. The workspace declares which Node.js version it uses, rbs downloads that exact
toolchain, and every target builds and runs against it — the same way on every developer's
machine and in CI.

Three rules cover most Node.js work:

| Rule             | Purpose                                                          |
| ---------------- | ---------------------------------------------------------------- |
| `nodejs_binary`  | A runnable Node.js program (servers, CLIs, scripts)              |
| `nodejs_library` | Shared code other targets depend on                              |
| `nodejs_test`    | Tests, run with Jest or plain Node, with optional coverage gates |

For browser apps (React, bundling, dev servers), see [Web Apps](/languages/web-apps).
Component testing with `vitest_test` is covered [at the end of this page](#component-testing-with-vitest_test).

## Set up the toolchain

Declare the Node.js toolchain once in your workspace's `WORKSPACE.rbs`:

```python theme={null}
load("@rbs//nodejs/toolchain.rbs", "nodejs_toolchain")

nodejs_toolchain(name = "nodejs", version = "22.15.1")
```

rbs downloads the official Node.js release for your platform on first use and caches it.
Supported versions include `22.15.1` (default), `22.11.0`, `20.19.6`, `20.11.0`, and
`18.19.0`.

## Declare npm dependencies

rbs resolves npm packages itself, directly from the npm registry — there is no
`package.json`, no `npm install`, and no npm/yarn CLI involved. Each package your
workspace uses is declared in `WORKSPACE.rbs`:

```python theme={null}
load("@rbs//nodejs/dependencies.rbs", "nodejs_repository")

nodejs_repository(
    name = "express_repo",
    package = "express",
    version = "4.18.2",
)

nodejs_repository(
    name = "lodash_repo",
    package = "lodash",
    version = "4.17.21",
)

# Build-time tools run on the host machine — mark them with host = True
nodejs_repository(
    name = "typescript_repo",
    package = "typescript",
    version = "5.3.3",
    host = True,
)
```

Transitive dependencies are resolved automatically, exactly like npm would: rbs walks the
dependency tree, resolves semver ranges, and downloads everything in parallel into a shared
cache. `version` is usually an exact pin, but semver ranges (`"^4.18.0"`) and `"latest"`
also work — exact pins are recommended for reproducibility.

To declare several packages at once:

```python theme={null}
load("@rbs//nodejs/dependencies.rbs", "nodejs_repositories")

nodejs_repositories(
    name = "app_deps",
    packages = [
        "express:4.18.2",
        "cors:2.8.5",
        "morgan:1.10.0",
    ],
)
```

### The lockfile

The first resolution writes `rbs.lock` at the workspace root, pinning every resolved
package (including transitives). Later builds resolve instantly from the lockfile instead
of hitting the registry.

<Tip>
  Commit `rbs.lock` to version control. It makes dependency resolution reproducible and fast
  for everyone on the branch.
</Tip>

### Referencing dependencies from targets

In a `BUILD.rbs` file, a target's `deps` can reference npm packages two ways:

```python theme={null}
nodejs_binary(
    name = "server",
    srcs = ["server.js"],
    deps = [
        ":express_repo",                  # the repository target from WORKSPACE.rbs
        "@external://lodash:4.17.21",     # direct package:version label
    ],
)
```

Both forms resolve to the same cached package. The `@external://package:version` form is
explicit about the version at the point of use; the `:name_repo` form points back at the
`nodejs_repository` declaration.

<Note>
  When a target runs, your code sees an ordinary `node_modules` directory and imports resolve
  exactly as in any Node.js project — rbs stages and shares the packages behind the scenes.
</Note>

## nodejs\_binary

Builds a runnable Node.js program with a hermetic launcher: the target carries its own
Node.js toolchain and dependencies, so `rbs run` works on any machine with no setup.

```python theme={null}
load("@rbs//nodejs/rules.rbs", "nodejs_binary")

nodejs_binary(
    name = "server",
    srcs = ["server.js"],
    main = "server.js",
    deps = [
        ":routes",           # local nodejs_library
        ":express_repo",     # npm package
    ],
    env = {
        "NODE_ENV": "development",
        "PORT": "3000",
    },
)
```

```bash theme={null}
rbs build :server        # build it
rbs run :server          # build and run it
rbs run :server --watch  # auto-rebuild and restart on file changes
```

### Attributes

| Attribute             | Type        | Default                          | Description                                                 |
| --------------------- | ----------- | -------------------------------- | ----------------------------------------------------------- |
| `srcs`                | label list  | —                                | JavaScript/TypeScript source files                          |
| `deps`                | label list  | —                                | Dependencies: npm packages or local library targets         |
| `main`                | string      | first of `srcs`, else `index.js` | Main entry point file                                       |
| `data`                | label list  | —                                | Data files staged next to the sources                       |
| `args`                | string list | —                                | Default arguments passed to the program                     |
| `env`                 | string dict | —                                | Environment variables set at run time                       |
| `tsconfig`            | string      | —                                | Path to a `tsconfig.json` used for TypeScript compilation   |
| `ts_compiler_options` | string dict | —                                | TypeScript compiler options (see [TypeScript](#typescript)) |

## nodejs\_library

Groups sources into a reusable unit. Libraries record their own npm dependencies, and any
binary or test that depends on the library gets those packages automatically.

```python theme={null}
load("@rbs//nodejs/rules.rbs", "nodejs_library")

nodejs_library(
    name = "utils",
    srcs = [
        "lib/utils.js",
        "lib/logger.js",
    ],
    deps = [":lodash_repo"],
)

nodejs_binary(
    name = "app",
    srcs = ["main.js"],
    deps = [":utils"],   # lodash comes along transitively
)
```

Code in a consuming target imports library files by the library's target name followed by
the file's path within the library:

```javascript theme={null}
const { formatDate } = require("utils/lib/utils");
```

### Attributes

| Attribute             | Type        | Default | Description                                    |
| --------------------- | ----------- | ------- | ---------------------------------------------- |
| `srcs`                | label list  | —       | JavaScript/TypeScript source files             |
| `deps`                | label list  | —       | Dependencies (npm packages or other libraries) |
| `data`                | label list  | —       | Data files included with the library           |
| `tsconfig`            | string      | —       | Path to a `tsconfig.json`                      |
| `ts_compiler_options` | string dict | —       | TypeScript compiler options                    |

## TypeScript

TypeScript works out of the box in all three rules: when `srcs` contain `.ts` or `.tsx`
files, rbs compiles them with `tsc` before staging. The only requirement is that the
workspace declares the `typescript` package as a host dependency:

```python theme={null}
nodejs_repository(
    name = "typescript_repo",
    package = "typescript",
    version = "5.3.3",
    host = True,
)
```

The default compiler options target `ES2020` with CommonJS modules, `strict` mode, and
`esModuleInterop` enabled. Override individual options per target:

```python theme={null}
nodejs_library(
    name = "models",
    srcs = ["src/models/user.ts", "src/models/product.ts"],
    ts_compiler_options = {
        "target": "ES2022",
        "strict": True,
        "declaration": True,
    },
)
```

Or point the target at an existing config with `tsconfig = "tsconfig.json"`.

<Note>
  rbs also generates a TypeScript configuration for your **editor** (under `.rbs/lsp/`) so
  imports of npm packages and workspace libraries resolve in the IDE without any manual
  setup. This file is regenerated by builds — treat it as read-only. If you need different
  compiler behavior, set `ts_compiler_options` or `tsconfig` on the target rather than
  editing generated files.
</Note>

## Testing with nodejs\_test

`nodejs_test` runs your tests with one of two runners:

* **`jest`** (default) — real Jest: `describe`/`it`/`expect` suites, with Jest-collected
  coverage.
* **`node`** — plain Node.js: the test file is a self-executing script (e.g. using
  `assert`) that exits non-zero on failure; coverage is collected with NYC (Istanbul).

First, install the test dependencies once in `WORKSPACE.rbs`:

```python theme={null}
load("@rbs//nodejs/toolchain.rbs", "nodejs_toolchain", "nodejs_test_deps")

nodejs_toolchain(name = "nodejs", version = "22.15.1")
nodejs_test_deps()   # installs Jest, NYC, @types/jest, ts-jest
```

Then declare test targets:

```python theme={null}
load("@rbs//nodejs/rules.rbs", "nodejs_test")

# Jest suite — srcs are the test files; deps carry the code under test
nodejs_test(
    name = "calculator_test",
    srcs = ["test/calculator.jest.test.js"],
    deps = [":calculator"],
    test_runner = "jest",
)

# Plain-node script test
nodejs_test(
    name = "smoke_test",
    srcs = ["test/smoke.test.js"],
    deps = [":calculator"],
    test_runner = "node",
)
```

```bash theme={null}
rbs test :calculator_test      # run one test target
rbs test :all                  # all tests in the current package
rbs test //...                 # all tests in the workspace
rbs test :calculator_test -w   # watch mode: re-run on file changes (TDD)
```

<Tip>
  With `test_runner = "node"`, name your test files `*.test.js` so the coverage tool
  excludes them from the coverage report.
</Tip>

### Coverage gates

Set minimum coverage thresholds on the target and enforce them with `rbs coverage` — the
run **fails** if any metric drops below its threshold:

```python theme={null}
nodejs_test(
    name = "calculator_coverage_test",
    srcs = ["test/calculator.jest.test.js"],
    deps = [":calculator"],
    test_runner = "jest",
    min_line_coverage = 80,
    min_branch_coverage = 70,
    min_function_coverage = 75,
    size = "small",
    timeout = 60,
)
```

```bash theme={null}
rbs coverage :calculator_coverage_test   # collects lcov coverage, fails below thresholds
rbs coverage //...                       # coverage for every test target in the workspace
```

Coverage is real, measured lcov data — local library dependencies of the test are
instrumented too, so the numbers reflect everything the tests actually exercise.

### Attributes

| Attribute               | Type        | Default         | Description                                                                 |
| ----------------------- | ----------- | --------------- | --------------------------------------------------------------------------- |
| `srcs`                  | label list  | —               | Test source files                                                           |
| `deps`                  | label list  | —               | Code under test: npm packages or local libraries                            |
| `main`                  | string      | first of `srcs` | Main test file                                                              |
| `test_runner`           | string      | `"jest"`        | `"jest"` for describe/it/expect suites, `"node"` for self-executing scripts |
| `args`                  | string list | —               | Extra arguments for the runner                                              |
| `env`                   | string dict | —               | Environment variables                                                       |
| `size`                  | string      | `"medium"`      | Test size: `small`, `medium`, `large`                                       |
| `timeout`               | int         | `300`           | Timeout in seconds                                                          |
| `min_line_coverage`     | int         | `0`             | Minimum line coverage % — coverage run fails below it                       |
| `min_branch_coverage`   | int         | `0`             | Minimum branch coverage %                                                   |
| `min_function_coverage` | int         | `0`             | Minimum function coverage %                                                 |
| `tsconfig`              | string      | —               | Path to a `tsconfig.json`                                                   |
| `ts_compiler_options`   | string dict | —               | TypeScript compiler options                                                 |

## Component testing with vitest\_test

For React/DOM component tests, use `vitest_test` from the web-vite ruleset. It runs real
Vitest with a jsdom environment by default, transforms TS/TSX with Vitest's own pipeline
(no separate compile step), and collects V8 lcov coverage.

Install its dependencies once in `WORKSPACE.rbs`:

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

vitest_test_deps()   # vitest, @vitest/coverage-v8, jsdom, @testing-library/*
```

Then declare test targets — `srcs` must list both the test files (named `*.test.*` or
`*.spec.*`) and the sources they exercise:

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

vitest_test(
    name = "counter_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,
    min_branch_coverage = 70,
    min_function_coverage = 75,
)
```

```bash theme={null}
rbs test :counter_test       # run the suite
rbs coverage :counter_test   # enforce the coverage thresholds
```

<Note>
  `@testing-library/react` v16 requires `@testing-library/dom` as a peer dependency — list
  it explicitly in `deps`, as above.
</Note>

### Attributes

| Attribute               | Type        | Default    | Description                                                                                                                  |
| ----------------------- | ----------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `srcs`                  | label list  | —          | Test files (`*.test.*` / `*.spec.*`) plus the sources they exercise; relative paths are preserved                            |
| `deps`                  | label list  | —          | npm dependencies the tests need (react, testing-library, …)                                                                  |
| `setup_files`           | label list  | —          | Vitest `setupFiles` (e.g. a setup script importing `@testing-library/jest-dom`)                                              |
| `environment`           | string      | `"jsdom"`  | Vitest environment: `jsdom`, `node`, or `happy-dom`                                                                          |
| `lib_deps`              | string dict | —          | Workspace library deps `{ import_scope: target_label }` — same scopes as the web app rules                                   |
| `lib_dep_targets`       | label list  | —          | The `lib_deps` labels repeated as labels, to establish build ordering                                                        |
| `aliases`               | string dict | —          | Import-specifier aliases `{ specifier: file }` pointing at a stub listed in `srcs` (e.g. to stub a module-federation remote) |
| `args`                  | string list | —          | Extra Vitest CLI arguments                                                                                                   |
| `env`                   | string dict | —          | Environment variables                                                                                                        |
| `size`                  | string      | `"medium"` | Test size: `small`, `medium`, `large`                                                                                        |
| `timeout`               | int         | `300`      | Timeout in seconds                                                                                                           |
| `min_line_coverage`     | int         | `0`        | Minimum line coverage % — `rbs coverage` fails below it                                                                      |
| `min_branch_coverage`   | int         | `0`        | Minimum branch coverage %                                                                                                    |
| `min_function_coverage` | int         | `0`        | Minimum function coverage %                                                                                                  |

To test components that import a shared workspace library, declare the library under the
same import scope the app rules use:

```python theme={null}
vitest_test(
    name = "test",
    srcs = glob(["src/**/*.tsx", "src/**/*.ts", "src/**/*.css"]),
    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",
    ],
    lib_deps = {"@acme/ui": "//client/libs/ui:ui"},
    lib_dep_targets = ["//client/libs/ui:ui"],
    environment = "jsdom",
    min_line_coverage = 80,
)
```

The tests can then `import { Button } from '@acme/ui'` just like the application code
does. See [Web Apps](/languages/web-apps) for how libraries and apps are wired together.
