From cf173d09145e8b7291d960e5596fbecc3f4b2ba7 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 4 Mar 2026 10:47:52 +0100 Subject: [PATCH] feat: bun dev target support --- MODULE.bazel.lock | 2 +- README.md | 37 ++++++++++ bun/defs.bzl | 3 + examples/basic/BUILD.bazel | 8 +++ examples/basic/README.md | 10 ++- examples/basic/main.ts | 1 + internal/BUILD.bazel | 1 + internal/bun_dev.bzl | 142 +++++++++++++++++++++++++++++++++++++ 8 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 examples/basic/main.ts create mode 100644 internal/bun_dev.bzl diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 962e12e..51c7a9a 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -190,7 +190,7 @@ "//bun:extensions.bzl%bun": { "general": { "bzlTransitiveDigest": "Q0uQOwFAgAU+etePCZ4TUDO+adLX7Z0EmRLaEsKgncw=", - "usagesDigest": "qk1PDh3WICa0VONYKXJLsmWCesNJxz3Jkb/aH/voIeI=", + "usagesDigest": "UC4zk8kEwWRiDG5FVQOCFysXcrZ757Jehf3sZgG893w=", "recordedInputs": [ "REPO_MAPPING:,bazel_tools bazel_tools" ], diff --git a/README.md b/README.md index 26a4f7a..22aaeba 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ load( "@rules_bun//bun:defs.bzl", "bun_binary", "bun_bundle", + "bun_dev", "bun_test", "js_library", "ts_library", @@ -101,3 +102,39 @@ Run one of your bun-backed targets, for example: ```bash bazel test //path/to:your_bun_test ``` + +## Development mode (`bun_dev`) + +Use `bun_dev` for long-running local development with Bun watch mode. + +```starlark +load("@rules_bun//bun:defs.bzl", "bun_dev") + +bun_dev( + name = "web_dev", + entry_point = "src/main.ts", +) +``` + +Run it with: + +```bash +bazel run //path/to:web_dev +``` + +`bun_dev` supports: + +- `watch_mode = "watch"` (default) for `bun --watch` +- `watch_mode = "hot"` for `bun --hot` +- `restart_on = [...]` to force full process restarts when specific files change + +### Hybrid Go + Bun + protobuf workflow + +For monorepos that mix Go and Bun (including FFI): + +1. Run Bun app with native watch/HMR via `bun_dev`. +2. Put generated artifacts or bridge files in `restart_on` (for example generated JS/TS files from proto/go steps). +3. Rebuild Go/proto artifacts separately (for example with `ibazel build`) so their output files change. +4. `bun_dev` detects those `restart_on` changes and restarts Bun, while ordinary JS edits continue to use Bun watch/HMR without full Bazel restarts. + +This keeps the fast Bun JS loop while still supporting full restarts when non-JS dependencies change. diff --git a/bun/defs.bzl b/bun/defs.bzl index 91bfc90..739e89a 100644 --- a/bun/defs.bzl +++ b/bun/defs.bzl @@ -1,5 +1,7 @@ +"""Public API surface for Bun Bazel rules.""" load("//internal:bun_binary.bzl", _bun_binary = "bun_binary") load("//internal:bun_bundle.bzl", _bun_bundle = "bun_bundle") +load("//internal:bun_dev.bzl", _bun_dev = "bun_dev") load("//internal:bun_test.bzl", _bun_test = "bun_test") load("//internal:js_library.bzl", _js_library = "js_library", _ts_library = "ts_library") load(":toolchain.bzl", _BunToolchainInfo = "BunToolchainInfo", _bun_toolchain = "bun_toolchain") @@ -8,6 +10,7 @@ visibility("public") bun_binary = _bun_binary bun_bundle = _bun_bundle +bun_dev = _bun_dev bun_test = _bun_test js_library = _js_library ts_library = _ts_library diff --git a/examples/basic/BUILD.bazel b/examples/basic/BUILD.bazel index 6ee1624..b17b5c6 100644 --- a/examples/basic/BUILD.bazel +++ b/examples/basic/BUILD.bazel @@ -1,5 +1,13 @@ +load("//bun:defs.bzl", "bun_dev") + package(default_visibility = ["//visibility:public"]) exports_files([ "README.md", + "main.ts", ]) + +bun_dev( + name = "web_dev", + entry_point = "main.ts", +) diff --git a/examples/basic/README.md b/examples/basic/README.md index cba1cbf..fc59106 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -1,3 +1,11 @@ # basic example -Placeholder for end-to-end bun rules example. +Minimal `bun_dev` example. + +Run: + +```bash +bazel run //examples/basic:web_dev +``` + +This starts Bun in watch mode for `main.ts`. diff --git a/examples/basic/main.ts b/examples/basic/main.ts new file mode 100644 index 0000000..e5d1d96 --- /dev/null +++ b/examples/basic/main.ts @@ -0,0 +1 @@ +console.log("rules_bun bun_dev example"); diff --git a/internal/BUILD.bazel b/internal/BUILD.bazel index 5459801..c97a1a9 100644 --- a/internal/BUILD.bazel +++ b/internal/BUILD.bazel @@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"]) exports_files([ "bun_binary.bzl", "bun_bundle.bzl", + "bun_dev.bzl", "bun_install.bzl", "bun_test.bzl", "js_library.bzl", diff --git a/internal/bun_dev.bzl b/internal/bun_dev.bzl new file mode 100644 index 0000000..deb9245 --- /dev/null +++ b/internal/bun_dev.bzl @@ -0,0 +1,142 @@ +"""Rule for running JS/TS scripts with Bun in watch mode for development.""" + + +def _bun_dev_impl(ctx): + toolchain = ctx.toolchains["//bun:toolchain_type"] + bun_bin = toolchain.bun.bun_bin + entry_point = ctx.file.entry_point + + restart_watch_paths = "\n".join([path.short_path for path in ctx.files.restart_on]) + + launcher = ctx.actions.declare_file(ctx.label.name) + ctx.actions.write( + output = launcher, + is_executable = True, + content = """#!/usr/bin/env bash +set -euo pipefail + +runfiles_dir="${{RUNFILES_DIR:-$0.runfiles}}" +bun_bin="${{runfiles_dir}}/_main/{bun_short_path}" +entry_point="${{runfiles_dir}}/_main/{entry_short_path}" +cd "${{runfiles_dir}}/_main" + +watch_mode="{watch_mode}" +if [[ "${{watch_mode}}" == "hot" ]]; then + dev_flag="--hot" +else + dev_flag="--watch" +fi + +run_dev() {{ + exec "${{bun_bin}}" "${{dev_flag}}" run "${{entry_point}}" "$@" +}} + +if [[ {restart_count} -eq 0 ]]; then + run_dev "$@" +fi + +readarray -t restart_paths <<'EOF_RESTART_PATHS' +{restart_watch_paths} +EOF_RESTART_PATHS + +file_mtime() {{ + local p="$1" + if stat -f '%m' "${{p}}" >/dev/null 2>&1; then + stat -f '%m' "${{p}}" + return 0 + fi + stat -c '%Y' "${{p}}" +}} + +declare -A mtimes +for rel in "${{restart_paths[@]}}"; do + if [[ -e "${{rel}}" ]]; then + mtimes["${{rel}}"]="$(file_mtime "${{rel}}")" + else + mtimes["${{rel}}"]="missing" + fi +done + +child_pid="" +restart_child() {{ + if [[ -n "${{child_pid}}" ]] && kill -0 "${{child_pid}}" 2>/dev/null; then + kill "${{child_pid}}" + wait "${{child_pid}}" || true + fi + "${{bun_bin}}" "${{dev_flag}}" run "${{entry_point}}" "$@" & + child_pid=$! +}} + +cleanup() {{ + if [[ -n "${{child_pid}}" ]] && kill -0 "${{child_pid}}" 2>/dev/null; then + kill "${{child_pid}}" + wait "${{child_pid}}" || true + fi +}} + +trap cleanup EXIT INT TERM + +restart_child "$@" + +while true; do + sleep 1 + changed=0 + for rel in "${{restart_paths[@]}}"; do + if [[ -e "${{rel}}" ]]; then + current="$(file_mtime "${{rel}}")" + else + current="missing" + fi + if [[ "${{current}}" != "${{mtimes[${{rel}}]}}" ]]; then + mtimes["${{rel}}"]="${{current}}" + changed=1 + fi + done + if [[ "${{changed}}" -eq 1 ]]; then + restart_child "$@" + fi +done +""".format( + bun_short_path = bun_bin.short_path, + entry_short_path = entry_point.short_path, + watch_mode = ctx.attr.watch_mode, + restart_count = len(ctx.files.restart_on), + restart_watch_paths = restart_watch_paths, + ), + ) + + transitive_files = [] + if ctx.attr.node_modules: + transitive_files.append(ctx.attr.node_modules[DefaultInfo].files) + + runfiles = ctx.runfiles( + files = [bun_bin, entry_point] + ctx.files.data + ctx.files.restart_on, + transitive_files = depset(transitive = transitive_files), + ) + + return [ + DefaultInfo( + executable = launcher, + runfiles = runfiles, + ), + ] + + +bun_dev = rule( + implementation = _bun_dev_impl, + attrs = { + "entry_point": attr.label( + mandatory = True, + allow_single_file = [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"], + ), + "watch_mode": attr.string( + default = "watch", + values = ["watch", "hot"], + ), + "restart_on": attr.label_list(allow_files = True), + "node_modules": attr.label(), + "data": attr.label_list(allow_files = True), + }, + executable = True, + toolchains = ["//bun:toolchain_type"], +)