7.6 KiB
Here's a comprehensive plan for implementing a Bazel-native bun_rules package:
bun_rules: Bazel-Native Bun Implementation Plan
What Is This?
A Bazel ruleset that integrates the Bun JavaScript runtime natively — similar to rules_nodejs but leveraging Bun's bundler, test runner, package manager, and runtime. The goal is hermetic, reproducible builds using Bun as the toolchain.
Phase 1: Repository Skeleton & Toolchain
Where to start. Every Bazel ruleset begins with the toolchain — nothing else works without it.
1.1 Repo Structure
bun_rules/
├── MODULE.bazel # Bzlmod module definition
├── WORKSPACE # Legacy workspace support
├── BUILD.bazel
├── bun/
│ ├── repositories.bzl # Download bun binaries per platform
│ ├── toolchain.bzl # bun_toolchain rule
│ └── defs.bzl # Public API re-exports
├── internal/
│ ├── bun_binary.bzl
│ ├── bun_test.bzl
│ ├── bun_install.bzl
│ └── bun_bundle.bzl
├── examples/
│ └── basic/
└── tests/
├── toolchain_test/
├── install_test/
├── binary_test/
└── bundle_test/
1.2 Toolchain Rule (toolchain.bzl)
BunToolchainInfo = provider(fields = ["bun_bin", "version"])
bun_toolchain = rule(
implementation = _bun_toolchain_impl,
attrs = {
"bun": attr.label(allow_single_file = True, executable = True, cfg = "exec"),
"version": attr.string(),
},
)
1.3 Binary Downloads (repositories.bzl)
Use http_file to fetch platform-specific Bun binaries:
bun-linux-x64,bun-linux-aarch64bun-darwin-x64,bun-darwin-aarch64bun-windows-x64.exe
Use SHA256 checksums pinned per Bun release. Register via register_toolchains().
Tests needed:
toolchain_resolution_test— assert the correct binary is selected per--platformsbun --versionsmoke test via ash_test
Phase 2: bun_install (Package Manager)
Replaces npm install / yarn. This is the highest-leverage rule because every downstream rule depends on it.
Rule Design
bun_install(
name = "node_modules",
package_json = "//:package.json",
bun_lockfile = "//:bun.lockb",
)
- Runs
bun install --frozen-lockfilein a sandboxed action - Outputs a
node_modules/directory as aTreeArtifact - Must be hermetic: no network in actions (vendor or use a repository rule to pre-fetch)
Key Challenges
bun.lockbis binary — you need to commit it and treat it as a source file- Network access during
bun installbreaks Bazel's sandbox; solve with either:- A repository rule that runs install at analysis time (like
npm_installin rules_nodejs) - Or a module extension in Bzlmod
- A repository rule that runs install at analysis time (like
Tests needed:
- Install succeeds with a valid
package.json+bun.lockb - Build fails (with a clear error) when
bun.lockbis out of date - Determinism test: run install twice, assert identical output digest
- Test that
node_modulesis correctly provided to downstream rules
Phase 3: bun_binary (Run JS/TS scripts)
bun_binary(
name = "my_script",
entry_point = "src/main.ts",
node_modules = "//:node_modules",
data = glob(["src/**"]),
)
- Wraps
bun run <entry>as a Bazel executable - Provides
DefaultInfowith a launcher script - Handles both
.jsand.tsnatively (no transpile step needed)
Tests needed:
bun_binaryproduces a runnable target (bazel run)- TypeScript entry points work without separate compilation
datadeps are available at runtime- Environment variables pass through correctly
Phase 4: bun_test (Test Runner)
bun_test(
name = "my_test",
srcs = ["src/foo.test.ts"],
node_modules = "//:node_modules",
)
- Wraps
bun testwith Bazel's test runner protocol - Must exit with code 0/non-0 correctly
- Outputs JUnit XML for
--test_outputcompatibility (usebun test --reporter junit)
Tests needed:
- Passing test suite returns exit 0
- Failing test suite returns exit non-0 (Bazel marks as FAILED)
- Test filtering via
--test_filterworks - Coverage via
bun test --coverageintegrates withbazel coverage - Tests are re-run when source files change (input tracking)
- Tests are not re-run when unrelated files change (cache correctness)
Phase 5: bun_bundle (Bundler)
bun_bundle(
name = "app_bundle",
entry_points = ["src/index.ts"],
node_modules = "//:node_modules",
target = "browser", # or "node", "bun"
format = "esm", # or "cjs", "iife"
minify = True,
)
- Runs
bun buildas a Bazel action - Outputs are declared files (JS, sourcemaps, assets)
- Supports splitting, external packages, define/env vars
Tests needed:
- Output file exists and has non-zero size
minify = Trueproduces smaller output thanminify = Falseexternalpackages are not bundled- Sourcemaps are generated when requested
- Build is hermetic: same inputs → identical output digest (content hash)
- Invalid entry point produces a clear build error (not a cryptic Bazel failure)
Phase 6: js_library / ts_library (Source Grouping)
Lightweight rules for grouping sources and propagating them through the dep graph:
ts_library(
name = "utils",
srcs = glob(["src/**/*.ts"]),
deps = [":node_modules"],
)
Tests needed:
depscorrectly propagate transitive sources tobun_bundleandbun_test- Circular dep detection (or at least graceful failure)
Required Tests Summary
| Category | Test |
|---|---|
| Toolchain | Correct binary resolves per platform |
| Toolchain | bun --version executes successfully |
bun_install |
Clean install works |
bun_install |
Stale lockfile fails with clear error |
bun_install |
Output is deterministic |
bun_binary |
JS entry point runs |
bun_binary |
TS entry point runs without compile step |
bun_binary |
Data files available at runtime |
bun_test |
Passing tests → exit 0 |
bun_test |
Failing tests → exit non-0 |
bun_test |
Cache hit: unchanged test not re-run |
bun_test |
Cache miss: changed source triggers re-run |
bun_test |
JUnit XML output parseable |
bun_bundle |
Output file produced |
bun_bundle |
Minification reduces output size |
bun_bundle |
Hermetic: identical inputs → identical digest |
bun_bundle |
External packages excluded correctly |
| Integration | examples/basic builds end-to-end with bazel build //... |
| Integration | bazel test //... passes all tests |
Development Sequence
1. Toolchain downloads + resolution ← start here
2. bun_install (repository rule approach)
3. bun_binary (simplest runtime rule)
4. bun_test
5. bun_bundle
6. js_library / ts_library
7. Bzlmod module extension for installs
8. CI matrix (linux-x64, darwin-arm64, windows)
9. Docs + examples
Where to Start Right Now
Day 1: Copy the pattern from rules_go or aspect-build/rules_js for toolchain registration. Write repositories.bzl that fetches the Bun binary for your current platform only. Write a sh_test that calls bun --version and asserts it exits 0. Get that green.
Reference implementations to study:
aspect-build/rules_js— best modern reference for JS in Bazelbazelbuild/rules_nodejs— older but battle-tested patternsbazelbuild/rules_python— excellent toolchain download pattern to copy
The toolchain is the entire foundation. Nothing else is possible without it being solid.