Files
rules_bun/implementation_plan.md
2026-03-04 09:09:18 +01:00

11 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-aarch64
  • bun-darwin-x64, bun-darwin-aarch64
  • bun-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 --platforms
  • bun --version smoke test via a sh_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-lockfile in a sandboxed action
  • Outputs a node_modules/ directory as a TreeArtifact
  • Must be hermetic: no network in actions (vendor or use a repository rule to pre-fetch)

Key Challenges

  • bun.lockb is binary — you need to commit it and treat it as a source file
  • Network access during bun install breaks Bazel's sandbox; solve with either:
    • A repository rule that runs install at analysis time (like npm_install in rules_nodejs)
    • Or a module extension in Bzlmod

Tests needed:

  • Install succeeds with a valid package.json + bun.lockb
  • Build fails (with a clear error) when bun.lockb is out of date
  • Determinism test: run install twice, assert identical output digest
  • Test that node_modules is 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 DefaultInfo with a launcher script
  • Handles both .js and .ts natively (no transpile step needed)

Tests needed:

  • bun_binary produces a runnable target (bazel run)
  • TypeScript entry points work without separate compilation
  • data deps 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 test with Bazel's test runner protocol
  • Must exit with code 0/non-0 correctly
  • Outputs JUnit XML for --test_output compatibility (use bun 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_filter works
  • Coverage via bun test --coverage integrates with bazel 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 build as 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 = True produces smaller output than minify = False
  • external packages 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:

  • deps correctly propagate transitive sources to bun_bundle and bun_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

Gap-Closing Checklist (Concrete Targets)

Use this checklist to close the current coverage gaps with explicit test targets.

Status Gap Proposed target Location
Partial Toolchain resolves per platform is only host-select tested toolchain_resolution_matrix_test tests/toolchain_test/BUILD.bazel
Missing bun_install deterministic output digest bun_install_determinism_test tests/install_test/BUILD.bazel
Missing bun_binary runtime data files availability bun_binary_data_test tests/binary_test/BUILD.bazel
Partial bun_test failing suite exists but is manual-only bun_test_failing_suite_test tests/bun_test_test/BUILD.bazel
Missing bun_test cache hit (unchanged inputs) bun_test_cache_hit_test tests/bun_test_test/BUILD.bazel
Missing bun_test cache miss (changed source) bun_test_cache_miss_test tests/bun_test_test/BUILD.bazel
Missing bun_test JUnit XML parseability bun_test_junit_output_test tests/bun_test_test/BUILD.bazel
Missing bun_bundle hermetic digest stability bundle_hermetic_digest_test tests/bundle_test/BUILD.bazel
Missing bun_bundle external package exclusion bundle_external_exclusion_test tests/bundle_test/BUILD.bazel
Missing examples/basic end-to-end build via Bazel examples_basic_e2e_build_test tests/integration_test/BUILD.bazel
Partial CI currently runs bazel test //tests/... only repo_all_targets_test tests/integration_test/BUILD.bazel

Recommended implementation order:

  1. bun_test_failing_suite_test (remove/manual split) and bun_binary_data_test
  2. bun_install_determinism_test, bundle_hermetic_digest_test
  3. bun_test_cache_hit_test, bun_test_cache_miss_test, bun_test_junit_output_test
  4. bundle_external_exclusion_test, examples_basic_e2e_build_test, repo_all_targets_test

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 Bazel
  • bazelbuild/rules_nodejs — older but battle-tested patterns
  • bazelbuild/rules_python — excellent toolchain download pattern to copy

The toolchain is the entire foundation. Nothing else is possible without it being solid.