tomato·bazeldocs v0 · latest
Docs/Reference/Modules/rules_bun

rules_bun

Bazel rules for Bun. Hermetic 'bun test' + sandbox-escaping 'bun run' against prebuilt binaries from oven-sh/bun releases.

Latest0.4.1
Versions6
CategoryBazel rules
Compat level1
MaintainersMatt Marshall
Registryhttps://registry.tbzl.dev/modules/rules_bun/
Sourcegithub.com/tomato-bazel/rules_bun
MODULE.bazelstarlark
bazel_dep(name = "rules_bun", version = "0.4.1")

View source & releases on GitHub ↗

Bazel rules for Bun. Fetches the prebuilt Bun binary, wraps it as a Bazel toolchain, and provides hermetic bun test + sandbox-escaping bun run runners, plus bun build bundling and bun build --compile standalone-executable rules.

  • module extensions (see docs/extensions.md):

    • bun — auto-creates @bun with the host-platform binary.
    • bun_deps — Bun-native node_modules staging. bun_deps.install(...) runs bun install --frozen-lockfile from a package.json + bun.lock and exposes the result as @<name>//:node_modules. The pure-Bun replacement for aspect_rules_js’s npm_translate_lock + npm_link_all_packagesno pnpm-lock, no aspect_rules_js.
  • toolchain: bun_toolchain — wraps the binary; resolved via @rules_bun//bun:toolchain_type. See docs/toolchains.md.

  • rules:

    • bun_test — runs bun test over listed source files as a Bazel test target (optional node_modules for dep resolution).
    • bun_runbazel run //path:target macro: invokes bun run <script> against the live workspace source.
    • bun_bundle — bundle a JS/TS entry point into one self-contained file via bun build (Bun-native node_modules path or legacy aspect driver path).
    • bun_compile — compile a JS/TS entry point into a standalone native executable via bun build --compile.

    See docs/defs.md.

Install

Add the registry to your .bazelrc:

common --registry=https://registry.fastverk.com/
common --registry=https://bcr.bazel.build/

In your MODULE.bazel:

bazel_dep(name = "rules_bun", version = "0.4.0")

bun = use_extension("@rules_bun//bun:extensions.bzl", "bun")
use_repo(bun, "bun")
register_toolchains("@bun//:bun_toolchain_def")

For the Bun-native dependency flow (recommended — no pnpm, no aspect_rules_js) you only need a package.json + bun.lock:

bun_deps = use_extension("@rules_bun//bun:extensions.bzl", "bun_deps")
bun_deps.install(
    name = "npm",
    package_json = "//:package.json",
    lock = "//:bun.lock",
)
use_repo(bun_deps, "npm")

bun_bundle / bun_compile then consume @npm//:node_modules directly (see below). The legacy path instead drives bun build through an aspect_rules_js js_binary, which needs aspect_rules_js (and a node toolchain) plus a pnpm-lock:

bazel_dep(name = "aspect_rules_js", version = "3.1.2")

Pin a specific version:

bun.toolchain(version = "1.3.14")

Quick start

Hermetic tests:

load("@rules_bun//bun:defs.bzl", "bun_test")

bun_test(
    name = "math_tests",
    srcs = glob(["*.test.ts"]),
    data = ["bunfig.toml"],
)

bazel test //:math_tests runs bun test <each src> with NO_COLOR=1 + DO_NOT_TRACK=1 set.

Dev runner:

load("@rules_bun//bun:defs.bzl", "bun_run")

bun_run(
    name = "build",
    script = "scripts/build.ts",
)

bazel run //:build -- --watch invokes bun run scripts/build.ts --watch against your live workspace source (not the Bazel sandbox). Useful for the dev loop where you want HMR / on-demand module resolution / filesystem watch outside the runfiles tree.

Bun-native flow (no aspect_rules_js, no pnpm-lock)

Stage node_modules with Bun and consume it from bun_test / bun_bundle. Your repo needs only package.json + bun.lock (generate the lock with bun install):

# MODULE.bazel
bun_deps = use_extension("@rules_bun//bun:extensions.bzl", "bun_deps")
bun_deps.install(
    name = "npm",
    package_json = "//:package.json",
    lock = "//:bun.lock",
)
use_repo(bun_deps, "npm")
# BUILD.bazel
load("@rules_bun//bun:defs.bzl", "bun_bundle", "bun_test")

# bun test resolves deps from the staged closure (no bun install).
bun_test(
    name = "unit",
    srcs = glob(["*.test.ts"]),
    node_modules = "@npm//:node_modules",
)

# bun build runs directly via the toolchain Bun — no js_binary driver.
bun_bundle(
    name = "bundle",
    srcs = ["index.ts"],          # entry + local modules
    entry = "pkg/index.ts",       # workspace-relative entry path
    out = "app.mjs",
    node_modules = "@npm//:node_modules",
    external = ["pg-native"],
)

bun_install fetches the sha-pinned host-platform Bun and runs bun install --frozen-lockfile; determinism comes from bun.lock. The build rules symlink the staged node_modules next to a real copy of the entry so Bun’s resolver walks up into it. See examples/install/ for the runnable end-to-end smoke.

Legacy aspect_rules_js flow

Bundle a JS/TS entry into one file:

load("@aspect_rules_js//js:defs.bzl", "js_binary")
load("@rules_bun//bun:defs.bzl", "bun_bundle")

# The driver js_binary stages the bundle entry + its full linked
# node_modules closure into runfiles; bun_bundle runs it as a build
# action so that closure materializes, then shells out to the hermetic
# Bun toolchain. `data` must list the entry's :lib + every npm-link dep.
js_binary(
    name = "bundle_driver",
    entry_point = "@rules_bun//bun:bun-build-driver",
    data = [":lib", ":node_modules/pg", ":node_modules/source-map-support"],
)

bun_bundle(
    name = "bundle",
    driver = ":bundle_driver",
    entry = "packages/api/index.js",
    out = "api.mjs",
    format = "esm",
    # Keep native addons / runtime requires out of the bundle.
    external = ["pg-native", "@aws-sdk/client-ssm", "encoding", "source-map-support"],
)

bazel build //:bundle emits a single self-contained api.mjs. Bun resolves the import graph from the staged node_modules natively (no bun install). The external modules are left as runtime requires rather than inlined — provide them alongside the bundle.

Compile a JS/TS entry into a standalone native executable:

load("@aspect_rules_js//js:defs.bzl", "js_binary")
load("@rules_bun//bun:defs.bzl", "bun_compile")

js_binary(
    name = "cli_driver",
    entry_point = "@rules_bun//bun:bun-build-driver",
    data = [":lib", ":node_modules/pg"],
)

bun_compile(
    name = "cli",
    driver = ":cli_driver",
    entry = "apps/cli/index.js",
    out = "cli",
    # Omit `target` to compile for the host; set it to cross-compile,
    # e.g. "bun-linux-x64-modern" for a linux OCI image.
    target = "bun-linux-x64-modern",
    external = ["pg-native"],
)

The output is a runnable executable (bazel run //:cli, or drop it into an OCI image). --compile bundles the Bun runtime + your JS into one file. Native .node addons are NOT embedded — keep them external and ship the .node files at runtime next to the binary.

Cross-target note: on a macOS dev host, target = "" compiles a Mach-O binary; CI on linux compiles an ELF. For a linux OCI image, set target = "bun-linux-x64-modern" (or the arm64 / musl variant) explicitly so the binary matches the image regardless of which host built it. A future enhancement could derive target from the Bazel --platforms via a transition; for now pass the string.

See examples/ for runnable bun_bundle + bun_compile smoke tests.

How it works

The module extension fetches a sha-pinned Bun binary for the host platform from oven-sh/bun GitHub releases. The release zip extracts to bun-<platform>/bun; the repository rule strips the outer dir so the binary lands at @bun//:bun.

bun_toolchain wraps that binary as a Bazel toolchain. bun_test resolves the toolchain via @rules_bun//bun:toolchain_type and runs bun test over each src in a runfiles-staged sandbox. bun_run is a macro that emits a sh_binary escaping the sandbox to run against BUILD_WORKSPACE_DIRECTORY directly.

The bun_deps extension’s bun_install repo rule fetches the same sha-pinned host-platform Bun, copies your package.json + bun.lock, and runs bun install --frozen-lockfile to materialize node_modules. Like aspect’s npm extension (and http_archive), a repo rule is allowed network I/O — --frozen-lockfile makes the result a pure function of the committed bun.lock, so the only fetch is what the lock pins. Lifecycle scripts are skipped (--ignore-scripts) unless you --trust a package via trusted_dependencies.

Hermeticity + determinism

LayerPinned by
Bun binarysha256 in bun/private/known_versions.bzl per (version, platform)
node_modulesbun.lock (consumed under bun install --frozen-lockfile; scripts off by default)
Test envbun_test sets NO_COLOR=1, DO_NOT_TRACK=1, BUN_INSTALL_NO_TRACK=1
bun_run envsame, with NO_COLOR overridable for callers that want colored output

bun_run is intentionally non-hermetic — Bun’s dev mode (HMR, watch, on-demand module resolution) needs filesystem access outside the runfiles tree. Counterpart to bun_test’s hermetic execution.

Compatibility

  • Bazel: 7.4+, bzlmod required.
  • Bun: 1.3.14 pinned by default. Bump via known_versions.bzl.
  • Platforms: darwin-aarch64, darwin-x64, linux-aarch64, linux-x64. Baseline + musl + Windows variants doable — add an entry to the table when needed.

Contributing

Reference docs (docs/{defs,extensions,toolchains}.md) are stardoc-generated. After editing rule docstrings:

bazel run //docs:update

CI gates this via bazel test //docs/....

License

MIT.

Usage#

Real usage, taken from the module’s examples/.

examples/BUILD.bazel

load("@examples_npm//:defs.bzl", "npm_link_all_packages")

package(default_visibility = ["//visibility:public"])

# Links the example pnpm workspace's node_modules at //examples, so Bun's
# resolver finds it by walking up from examples/bundle/ and examples/compile/.
npm_link_all_packages(name = "node_modules")

exports_files(["pnpm-lock.yaml"])

examples/bundle/BUILD.bazel

load("@aspect_rules_js//js:defs.bzl", "js_binary")
load("@rules_bun//bun:defs.bzl", "bun_bundle", "bun_test")

package(default_visibility = ["//visibility:public"])

# The driver js_binary stages the bundle entry (the .ts sources, transpiled by
# Bun directly) next to the linked node_modules closure in its runfiles, so when
# `bun_bundle` runs it the whole import graph resolves from the staged tree.
js_binary(
    name = "bundle_driver",
    data = [
        "greet.ts",
        "index.ts",
        "//examples:node_modules/is-number",
        "//examples:node_modules/source-map-support",
    ],
    entry_point = "@rules_bun//bun:bun-build-driver",
)

# Bundle examples/bundle/index.ts into one self-contained ESM file. is-number
# and ./greet are inlined; source-map-support stays external.
bun_bundle(
    name = "bundle",
    out = "app.mjs",
    driver = ":bundle_driver",
    entry = "examples/bundle/index.ts",
    external = ["source-map-support"],
    format = "esm",
    target = "node",
)

# Smoke test: the bundle exists, runs, inlines the dep + local module, and keeps
# the external un-inlined. Run from the `_main` runfiles root with the bundle
# staged as data.
bun_test(
    name = "bundle_test",
    srcs = ["bundle.test.ts"],
    data = [":bundle"],
)

examples/compile/BUILD.bazel

load("@aspect_rules_js//js:defs.bzl", "js_binary")
load("@rules_bun//bun:defs.bzl", "bun_compile")
load("@rules_shell//shell:sh_test.bzl", "sh_test")

package(default_visibility = ["//visibility:public"])

# Driver js_binary: stages the compile entry + its npm dep closure.
js_binary(
    name = "compile_driver",
    data = [
        "main.ts",
        "//examples:node_modules/is-number",
    ],
    entry_point = "@rules_bun//bun:bun-build-driver",
)

# Compile examples/compile/main.ts into a standalone native executable for the
# HOST platform (no `target`), so CI builds for its own OS/arch without cross
# toolchains. The output is itself runnable: `bazel run //examples/compile:app`.
bun_compile(
    name = "app",
    out = "app_host",
    driver = ":compile_driver",
    entry = "examples/compile/main.ts",
)

# Smoke test: the produced file is executable and runs.
sh_test(
    name = "app_test",
    srcs = ["run_test.sh"],
    args = ["$(rootpath :app)"],
    data = [":app"],
)

examples/install/BUILD.bazel

load("@rules_bun//bun:defs.bzl", "bun_bundle", "bun_test")

package(default_visibility = ["//visibility:public"])

# Pure-Bun flow: NO aspect_rules_js, NO pnpm-lock. The node_modules closure is
# staged by `bun_deps.install` (see //MODULE.bazel) from this directory's
# package.json + bun.lock via `bun install --frozen-lockfile`, and consumed
# below as `@install_npm//:node_modules`.

# Bundle examples/install/index.ts into one self-contained ESM file. `is-number`
# (from the staged node_modules) and `./greet` are inlined — Bun runs directly
# via the toolchain (no js_binary driver). This is the `node_modules`/native
# `bun_bundle` path.
bun_bundle(
    name = "bundle",
    srcs = [
        "greet.ts",
        "index.ts",
    ],
    out = "app.mjs",
    entry = "examples/install/index.ts",
    format = "esm",
    node_modules = "@install_npm//:node_modules",
    target = "node",
)

# Proof that `bun test` resolves a dep from the `bun_install` node_modules tree.
bun_test(
    name = "resolve_test",
    srcs = ["resolve.test.ts"],
    node_modules = "@install_npm//:node_modules",
)

# Smoke test that the produced bundle exists, runs, and inlined the dep.
bun_test(
    name = "bundle_test",
    srcs = ["bundle.test.ts"],
    data = [":bundle"],
)

exports_files([
    "package.json",
    "bun.lock",
])

Rules & providers#

Generated with Stardoc from the module's .bzl sources.

from docs/defs.md

User-facing rules for rules_bun.

Four pieces:

  • bun_test — runs bun test as a hermetic Bazel test action with explicit srcs + deps. Returns a BunTestInfo provider wrapping the test result file (for downstream consumers; the main consumer is the test framework, which only cares about exit codes).

  • bun_run — sh_binary macro: bazel run //path:NAME invokes bun run <script> against the live workspace source. Intentionally non-hermetic (escapes the runfiles sandbox) for the dev loop. Counterpart to bun_test’s hermetic execution.

  • bun_bundle — bundle a JS/TS entry point into one self-contained file with bun build. Returns BunBundleInfo.

  • bun_compile — compile a JS/TS entry point into a standalone native executable with bun build --compile (Bun runtime + bundled JS). Returns BunBinaryInfo and is bazel run-nable.

All resolve the Bun binary via @rules_bun//bun:toolchain_type (set up by register_toolchains("@bun//:bun_toolchain_def") in your MODULE.bazel).

bun_bundle / bun_compile have two ways to provision node_modules:

  • Bun-native (recommended; no aspect_rules_js, no pnpm-lock): pass a node_modules label (a @<name>//:node_modules from a bun_deps.install tag — see extensions.bzl) plus srcs (the entry

    • local modules). bun build runs directly via the toolchain Bun; a small shell driver stages the entry into a real tree and symlinks the closure so Bun resolves the import graph natively.
  • Legacy aspect_rules_js: pass a driver js_binary whose entry point is @rules_bun//bun:bun-build-driver and whose data stages the build entry plus its full linked node_modules closure; aspect materializes that closure into the action runfiles.

driver and node_modules are mutually exclusive — set exactly one. bun_test likewise takes an optional node_modules for dep resolution.

bun_bundle

load("@rules_bun//bun:defs.bzl", "bun_bundle")

bun_bundle(name, srcs, out, driver, entry, external, format, node_modules, target)

Bundle a JS/TS entry into one file via the hermetic Bun toolchain. Either Bun-native (node_modules from bun_deps.install, no aspect_rules_js) or the legacy aspect driver js_binary path.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
srcsBun-native path. The entry file + any local modules it imports, declared as action inputs. Ignored on the legacy driver path (that stages sources via the js_binary’s data).List of labelsoptional[]
outThe single bundled output file (conventionally *.mjs).Labelrequired
driverLEGACY aspect_rules_js path. A js_binary whose entry point is @rules_bun//bun:bun-build-driver and whose data stages the bundle entry + its full linked node_modules closure. Mutually exclusive with node_modules; set exactly one.LabeloptionalNone
entryPath of the entry point relative to the workspace root (e.g. packages/aion-cli/index.js). On the native path this is the execroot-relative path; on the legacy path it is relative to the driver’s _main runfiles root (same string in practice).Stringrequired
externalModule names to exclude from the bundle (passed as --external <name>, repeatable). Use for native addons and runtime requires that must stay external, e.g. pg-native, @aws-sdk/*, encoding, source-map-support.List of stringsoptional[]
formatBun --format. Defaults to esm so import.meta in deps stays valid under Node.Stringoptional"esm"
node_modulesBun-native path. A node_modules closure (typically @<name>//:node_modules from a bun_deps.install tag). When set, bun build runs directly via the toolchain Bun (no js_binary driver, no aspect_rules_js): the closure is symlinked to the execroot root so Bun resolves the import graph by walking up from entry. Mutually exclusive with driver. Pair with srcs (the entry + local modules).LabeloptionalNone
targetBun --target: the intended execution environment for the bundle. Defaults to node.Stringoptional"node"

bun_compile

load("@rules_bun//bun:defs.bzl", "bun_compile")

bun_compile(name, srcs, out, driver, entry, external, node_modules, target)

Compile a JS/TS entry into a standalone native executable (Bun runtime + bundled JS) via bun build --compile. Either Bun-native (node_modules) or the legacy aspect driver path.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
srcsBun-native path. The entry file + any local modules it imports, declared as action inputs. Ignored on the legacy driver path.List of labelsoptional[]
outThe standalone executable output. On --target bun-windows-* give it a .exe suffix.Labelrequired
driverLEGACY aspect_rules_js path. A js_binary whose entry point is @rules_bun//bun:bun-build-driver and whose data stages the build entry + its full linked node_modules closure. Mutually exclusive with node_modules; set exactly one.LabeloptionalNone
entryPath of the entry point relative to the workspace root (e.g. apps/studio-cli/index.js).Stringrequired
externalModule names to keep external (--external <name>, repeatable). NOTE: native .node addons are NOT embedded by --compile — list them here and provide the .node files at runtime alongside the produced binary.List of stringsoptional[]
node_modulesBun-native path. A node_modules closure (typically @<name>//:node_modules from a bun_deps.install tag). When set, bun build --compile runs directly via the toolchain Bun (no js_binary driver, no aspect_rules_js). Mutually exclusive with driver. Pair with srcs.LabeloptionalNone
targetBun compile target triple. Empty (the default) compiles for the host platform. Cross-compile values: bun-linux-x64, bun-linux-x64-modern, bun-linux-x64-baseline, bun-linux-arm64, bun-darwin-x64, bun-darwin-arm64, bun-windows-x64, and the *-musl libc variants (e.g. bun-linux-x64-musl). A future enhancement could derive this from the Bazel --platforms via a transition; for v1 pass the string.Stringoptional""

bun_test

load("@rules_bun//bun:defs.bzl", "bun_test")

bun_test(name, srcs, data, node_modules)

Run bun test over the listed source files as a Bazel test target.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
srcsTest files (typically *.test.ts, *.test.js). Each is passed to bun test explicitly so Bazel tracks them as inputs.List of labelsrequired
dataAdditional runtime inputs (fixtures, bunfig.toml, etc.).List of labelsoptional[]
node_modulesOptional node_modules closure (typically @<name>//:node_modules from a bun_deps.install tag). Staged at the workspace runfiles root as node_modules/ so bun test resolves dependency imports without bun install. The Bun-native replacement for aspect_rules_js’s npm_link_all_packages.LabeloptionalNone

BunBinaryInfo

load("@rules_bun//bun:defs.bzl", "BunBinaryInfo")

BunBinaryInfo(binary, target)

A standalone native executable produced by bun build --compile.

FIELDS

NameDescription
binaryFile: the standalone executable.
targetstring: the Bun compile target triple (empty = host).

BunBundleInfo

load("@rules_bun//bun:defs.bzl", "BunBundleInfo")

BunBundleInfo(bundle, format)

A single-file bundle produced by bun build.

FIELDS

NameDescription
bundleFile: the bundled output.
formatstring: the Bun output format (esm/cjs/iife).

BunTestInfo

load("@rules_bun//bun:defs.bzl", "BunTestInfo")

BunTestInfo(result)

Result metadata for a bun test run.

FIELDS

NameDescription
resultFile: the captured test output (stdout + stderr concatenated).

bun_run

load("@rules_bun//bun:defs.bzl", "bun_run")

bun_run(name, script, args, **kwargs)

Invoke bun run <script> against the live workspace source.

Escapes the runfiles sandbox via BUILD_WORKSPACE_DIRECTORY so Bun resolves modules + reads files from the user’s actual source tree. Intentionally NOT hermetic — that’s bun_test’s job.

PARAMETERS

NameDescriptionDefault Value
nametarget name.none
scriptpackage-relative path to the Bun script entry point.none
argsextra args passed to bun run after the script name.None
kwargsforwarded to the underlying sh_binary.none

from docs/extensions.md

Module extensions for rules_bun.

Two extensions:

  • bun — auto-fetches a prebuilt Bun binary for the host platform. Versions are sha256-pinned in private/known_versions.bzl. Consumers can override via the toolchain tag class.

    bun = use_extension("@rules_bun//bun:extensions.bzl", "bun")
    use_repo(bun, "bun")
    register_toolchains("@bun//:bun_toolchain_def")

    Pin a specific version:

    bun.toolchain(version = "1.3.14")
  • bun_deps — Bun-native node_modules staging. Each install tag produces a repo @<name> whose :node_modules filegroup is a bun install --frozen-lockfile-ed tree. The pure-Bun replacement for aspect_rules_js’s npm_translate_lock + npm_link_all_packages (no pnpm-lock, no aspect_rules_js):

    bun_deps = use_extension("@rules_bun//bun:extensions.bzl", "bun_deps")
    bun_deps.install(
        name = "npm",
        package_json = "//:package.json",
        lock = "//:bun.lock",
    )
    use_repo(bun_deps, "npm")

    then bun_test(node_modules = "@npm//:node_modules", ...) and bun_bundle(node_modules = "@npm//:node_modules", ...).

The actual release fetching is delegated to @rules_github//github:repositories.bzl%github_binary_repository so that the URL-shape + sha-pinning logic stays consistent across all our rules_* repos.

bun

bun = use_extension("@rules_bun//bun:extensions.bzl", "bun")
bun.toolchain(version)

Sets up @bun as a Bazel-fetched prebuilt Bun binary.

TAG CLASSES

toolchain

Attributes

NameDescriptionTypeMandatoryDefault
versionOverride Bun version. Defaults to the value in known_versions.bzl.Stringoptional""

bun_deps

bun_deps = use_extension("@rules_bun//bun:extensions.bzl", "bun_deps")
bun_deps.install(name, bun_version, ignore_scripts, install_flags, lock, package_json,
                 trusted_dependencies)

Bun-native node_modules staging — @<name>//:node_modules from a bun install --frozen-lockfile. Replaces aspect_rules_js’s npm_translate_lock + npm_link_all_packages for pure-Bun repos.

TAG CLASSES

install

Stage a node_modules tree from a package.json + bun.lock.

Attributes

NameDescriptionTypeMandatoryDefault
nameName of the generated repo. Reference its node_modules as @<name>//:node_modules.Namerequired
bun_versionBun version to fetch for the install. Empty = the toolchain extension’s default.Stringoptional""
ignore_scriptsSkip dependency lifecycle scripts (--ignore-scripts). Default True.BooleanoptionalTrue
install_flagsExtra raw flags appended to bun install.List of stringsoptional[]
lockThe bun.lock pinning the install (--frozen-lockfile).Labelrequired
package_jsonThe package.json to install from.Labelrequired
trusted_dependenciesPackages to --trust (run lifecycle scripts for) even when ignore_scripts is True.List of stringsoptional[]

from docs/toolchains.md

Toolchain rule for rules_bun.

bun_toolchain wraps a single Bun binary as a Bazel toolchain. Consumers (the bun_test and bun_run rules) resolve Bun through @rules_bun//bun:toolchain_type, so users can register custom Bun binaries (locally-built fork, alternate version, baseline-CPU variant) via register_toolchains(...) without modifying rule attrs.

The module extension at @rules_bun//bun:extensions.bzl generates a default toolchain (@bun//:bun_toolchain_def) wrapping the prebuilt binary. Users register it from MODULE.bazel:

register_toolchains("@bun//:bun_toolchain_def")

bun_toolchain

load("@rules_bun//bun:toolchains.bzl", "bun_toolchain")

bun_toolchain(name, bun)

Declare a Bun binary as a Bazel toolchain.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
bunPath to the Bun executable.Labelrequired

BunToolchainInfo

load("@rules_bun//bun:toolchains.bzl", "BunToolchainInfo")

BunToolchainInfo(bun)

The Bun binary, resolved via a toolchain.

FIELDS

NameDescription
bunFile: the bun executable.

Conformance#

No gate findings. 17 contested atoms. See how gating works or the full report.

Contested atoms

Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.

AtomResolved hereElsewhere
apple_support 1.24.2 2.2.0 ×1
aspect_bazel_lib 2.22.5 2.8.1 ×1
aspect_rules_js 3.1.2 2.1.3 ×1
bazel_lib 3.2.2 3.0.0 ×12
bazel_skylib 1.8.2 1.9.0 ×2
gawk 5.3.2.bcr.3 5.3.2.bcr.1 ×12
jq.bzl 0.4.0 0.1.0 ×12
nlohmann_json 3.6.1 3.12.0.bcr.1 ×1
package_metadata 0.0.2 0.0.5 ×3
protobuf 33.4 34.0.bcr.1 ×2
rules_jvm_external 6.7 6.8 ×4
rules_nodejs 6.7.4 6.3.0 ×16.7.3 ×1
rules_python 1.7.0 2.0.1 ×1
rules_swift 3.1.2 3.6.1 ×1
tar.bzl 0.10.4 0.5.1 ×110.6.0 ×1
upb 0.0.0-20220923-a547704 0.0.0-20230516-61a97ef ×1
yq.bzl 0.3.4 0.1.1 ×12

Dependencies#

rules_bun in the registry graph — what it depends on (left) and what depends on it (right).

Depends on

platforms1.0.0bazel_skylib1.8.2rules_github0.1.1rules_shell0.6.1aspect_rules_js3.1.2stardoc0.7.2devrules_nodejs6.7.4dev

Used by (2 in the registry)

Versions#

6 published versions, newest first. Each resolves to an immutable, integrity-checked archive.

VersionIntegrity (sha256)Source archive
0.4.1 latest C4KdZFgzKbHzf91s… tag archive ↗
0.4.0 D0PyvOw9UVhiss2m… tag archive ↗
0.3.0 X0vFlHNWGLiDKYzU… tag archive ↗
0.2.1 GgO76UiD1AY1/TyV… tag archive ↗
0.2.0 50pq4WmTCNFNK94K… tag archive ↗
0.1.0 IeRwaRUtshXq6NpX… tag archive ↗

Changelog#

All notable changes to rules_bun. The format is loosely Keep a Changelog — version headers mirror the published bazel-registry entries.

0.4.0 — add bun_install (Bun-native node_modules; drop pnpm + aspect_rules_js)

  • New bun_deps module extension with an install tag — a Bun-native replacement for aspect_rules_js’s npm_translate_lock + npm_link_all_packages. bun_deps.install(name, package_json, lock) produces a repo @<name> whose :node_modules filegroup is an installed node_modules tree. The backing repo rule (bun_install) fetches a host-platform Bun (the same sha-pinned binary the toolchain extension uses, via known_versions.bzl), copies the consumer’s package.json + bun.lock into the repo root, and runs bun install --frozen-lockfile --no-progress with a repo-pinned BUN_INSTALL_CACHE_DIR and --ignore-scripts (opt back in per package via trusted_dependencies). package.json + bun.lock are read as rule inputs so edits re-trigger the install; determinism comes from the lockfile (the only network I/O is the registry fetch the lock pins, exactly like aspect’s npm extension + http_archive). So a pure-Bun repo needs ONLY package.json + bun.lock — no pnpm-lock, no aspect_rules_js.
  • bun_test gains an optional node_modules attr (a @<name>//:node_modules label). When set, the closure is staged so bun test resolves dependency imports with no bun install. Because Bazel stages the test files as symlinks into the read-only source tree and Bun’s resolver follows an entry’s realpath, the runner copies the test files into a real staging dir and symlinks node_modules at its root so resolution stays inside the staged tree.
  • bun_bundle + bun_compile gain a Bun-native path: pass node_modules (+ srcs for the entry + local modules) instead of a driver js_binary. On this path bun build runs directly via the toolchain Bun (no js_binary driver, no aspect_rules_js) — a small shell driver stages the entry into a real tree and symlinks the closure so Bun resolves the import graph. driver is now optional and mutually exclusive with node_modules; the legacy aspect path is unchanged for back-compat.
  • examples/install/: a pure-Bun end-to-end smoke (one npm dep is-number, a local module) with package.json + bun.lock, a bun_deps.install, and a bun_bundle + two bun_tests consuming @install_npm//:node_modules — NO aspect_rules_js, NO pnpm-lock. Proves the flow: bazel build //examples/install:bundle + bazel test //examples/install:resolve_test //examples/install:bundle_test.

0.3.0 — add bun_bundle + bun_compile

  • New bun_bundle rule: bundle a JS/TS entry point into one self-contained file via bun build. Takes a driver js_binary (entry point @rules_bun//bun:bun-build-driver) whose data stages the build entry plus its full linked node_modules closure; aspect_rules_js materializes that closure into the action’s runfiles so Bun resolves the import graph natively (no bun install). Attrs: format (esm|cjs|iife, default esm), target (node|browser|bun, default node), and external (a repeatable --external <name> list for native addons / runtime requires like pg-native, @aws-sdk/*, encoding, source-map-support). Returns BunBundleInfo.
  • New bun_compile rule: compile a JS/TS entry point into a standalone native executable (Bun runtime + bundled JS) via bun build --compile. Shares the driver with bun_bundle (via a --compile flag). The output is itself runnable, so bazel run //pkg:target works. Attrs: target (a Bun compile target triple such as bun-linux-x64-modern / bun-darwin-arm64; empty = host platform) and external. Returns BunBinaryInfo. Native .node addons are not embedded by --compile — keep them external and ship them at runtime alongside the binary.
  • bun-build-driver.mjs: a single shared driver for both rules, wrapped in a public js_library at @rules_bun//bun:bun-build-driver. It re-anchors --bun/--out on $JS_BINARY__EXECROOT, chdirs into the _main runfiles root, and invokes the hermetic Bun toolchain.
  • aspect_rules_js is now a (non-dev) bazel_dep — consumers already bring it to declare the driver js_binary.
  • examples/: a bun_bundle smoke test (npm dep + local module + one external, asserts the bundle runs and the external is not inlined) and a host-target bun_compile smoke test (asserts the produced file is executable and runs). CI now runs bazel test //....

0.2.1 — fix bun_test toolchain runfiles path under bzlmod

  • bun_test’s generated runner failed to locate the hermetic Bun binary under bzlmod, exiting 127 (exec: : not found). Under bzlmod the toolchain Bun is an external repo file whose short_path is ../rules_bun++bun+bun/bun; the runner built BUN_BIN as ${RUNFILES_DIR}/<short_path>, so the leading ../ escaped the runfiles tree. Prefix with _main/ (${RUNFILES_DIR}/_main/<short_path>) so the embedded ../ resolves back out to the sibling external repo.
  • Make the find fallback follow symlinks (find -L) so it can reach the symlinked Bun binary in the runfiles tree.
  • Resolve the srcs test-filter paths from the same ${RUNFILES_DIR} base as BUN_BIN (was $0.runfiles). Under bazel test Bazel sets RUNFILES_DIR and $0 is the already-staged in-runfiles script, so $0.runfiles double-appended .runfiles/_main, yielding a test filter with no matches.

0.2.0 — delegate release fetching to rules_github

  • Replace the in-tree GitHub release download logic with a dependency on rules_github’s github_binary_repository so Bun binaries are fetched via the shared substrate alongside other fastverk rules.

0.1.0 — initial release

  • First cut of Bazel rules for Bun: a bun module extension that auto-creates @bun with the host-platform binary, a bun_toolchain resolved via @rules_bun//bun:toolchain_type, plus bun_test (hermetic) and bun_run (sandbox-escaping) rules.

← All modules