From d7a6d6b0ba2570e16fbf90d45ff42b1e135b615d Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 8 Mar 2026 03:29:54 +0100 Subject: [PATCH 1/4] fix: bun monorepo complex deps --- MODULE.bazel | 7 + MODULE.bazel.lock | 15 +- internal/bun_install.bzl | 49 +++- internal/bun_script.bzl | 138 ++++++++++- tests/install_test/BUILD.bazel | 19 ++ tests/install_test/workspaces_catalog.sh | 81 +++++++ tests/script_test/BUILD.bazel | 61 ++++- tests/script_test/paraglide_monorepo/bun.lock | 218 ++++++++++++++++++ .../paraglide_monorepo/package.json | 17 ++ .../packages/app-a/index.html | 12 + .../paraglide_monorepo/packages/app-a/main.js | 9 + .../packages/app-a/package.json | 14 ++ .../packages/app-a/vite.config.js | 10 + .../packages/app-b/index.html | 12 + .../paraglide_monorepo/packages/app-b/main.js | 6 + .../packages/app-b/package.json | 14 ++ .../packages/app-b/vite.config.js | 10 + .../packages/i18n/messages/en.json | 5 + .../packages/i18n/messages/sv.json | 5 + .../packages/i18n/package.json | 15 ++ .../i18n/project.inlang/settings.json | 15 ++ .../scripts/build-app-a.mjs | 14 ++ .../scripts/build-app-b.mjs | 14 ++ .../run_paraglide_monorepo_builds.sh | 54 +++++ 24 files changed, 804 insertions(+), 10 deletions(-) create mode 100755 tests/install_test/workspaces_catalog.sh create mode 100644 tests/script_test/paraglide_monorepo/bun.lock create mode 100644 tests/script_test/paraglide_monorepo/package.json create mode 100644 tests/script_test/paraglide_monorepo/packages/app-a/index.html create mode 100644 tests/script_test/paraglide_monorepo/packages/app-a/main.js create mode 100644 tests/script_test/paraglide_monorepo/packages/app-a/package.json create mode 100644 tests/script_test/paraglide_monorepo/packages/app-a/vite.config.js create mode 100644 tests/script_test/paraglide_monorepo/packages/app-b/index.html create mode 100644 tests/script_test/paraglide_monorepo/packages/app-b/main.js create mode 100644 tests/script_test/paraglide_monorepo/packages/app-b/package.json create mode 100644 tests/script_test/paraglide_monorepo/packages/app-b/vite.config.js create mode 100644 tests/script_test/paraglide_monorepo/packages/i18n/messages/en.json create mode 100644 tests/script_test/paraglide_monorepo/packages/i18n/messages/sv.json create mode 100644 tests/script_test/paraglide_monorepo/packages/i18n/package.json create mode 100644 tests/script_test/paraglide_monorepo/packages/i18n/project.inlang/settings.json create mode 100644 tests/script_test/paraglide_monorepo/scripts/build-app-a.mjs create mode 100644 tests/script_test/paraglide_monorepo/scripts/build-app-b.mjs create mode 100755 tests/script_test/run_paraglide_monorepo_builds.sh diff --git a/MODULE.bazel b/MODULE.bazel index bcd6ef2..a8f360c 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -37,6 +37,13 @@ bun_install_ext.install( ) use_repo(bun_install_ext, "script_test_vite_monorepo_node_modules") +bun_install_ext.install( + name = "script_test_paraglide_monorepo_node_modules", + bun_lockfile = "//tests/script_test:paraglide_monorepo/bun.lock", + package_json = "//tests/script_test:paraglide_monorepo/package.json", +) +use_repo(bun_install_ext, "script_test_paraglide_monorepo_node_modules") + bun_install_ext.install( name = "examples_vite_monorepo_node_modules", bun_lockfile = "//examples/vite_monorepo:bun.lock", diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index d9cea6e..8c3b8c2 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -192,7 +192,7 @@ "moduleExtensions": { "//bun:extensions.bzl%bun": { "general": { - "bzlTransitiveDigest": "64B4fTkEHdAlieIOkE/Wi2M/R9lMNZhFxeI1eXEFHRs=", + "bzlTransitiveDigest": "eSFVebwDN61an1dp3505njvMKN961HH+iY2tK6fEBQQ=", "usagesDigest": "/0BcCMA6AOzLhQaRK6DquxrCfpPHJUjSUaFz4zmQrsM=", "recordedInputs": [ "REPO_MAPPING:,bazel_tools bazel_tools" @@ -253,8 +253,8 @@ }, "//bun:extensions.bzl%bun_install": { "general": { - "bzlTransitiveDigest": "64B4fTkEHdAlieIOkE/Wi2M/R9lMNZhFxeI1eXEFHRs=", - "usagesDigest": "d+DGTyl4FpB6Ygb/R/V5knxm9bGYZKO223wMX1Q6R6w=", + "bzlTransitiveDigest": "eSFVebwDN61an1dp3505njvMKN961HH+iY2tK6fEBQQ=", + "usagesDigest": "f9pNm3AOxJDZmpHhL2vrrCo23IW33im/l/VYCTW2BWM=", "recordedInputs": [ "REPO_MAPPING:,bazel_tools bazel_tools" ], @@ -277,6 +277,15 @@ "isolated_home": true } }, + "script_test_paraglide_monorepo_node_modules": { + "repoRuleId": "@@//internal:bun_install.bzl%bun_install_repository", + "attributes": { + "package_json": "@@//tests/script_test:paraglide_monorepo/package.json", + "bun_lockfile": "@@//tests/script_test:paraglide_monorepo/bun.lock", + "install_inputs": [], + "isolated_home": true + } + }, "examples_vite_monorepo_node_modules": { "repoRuleId": "@@//internal:bun_install.bzl%bun_install_repository", "attributes": { diff --git a/internal/bun_install.bzl b/internal/bun_install.bzl index 6510ca4..e84fcbb 100644 --- a/internal/bun_install.bzl +++ b/internal/bun_install.bzl @@ -96,6 +96,53 @@ def _workspace_patterns(repository_ctx, package_json): return patterns +def _validate_catalog_shape(field, value): + if value == None: + return + + if type(value) != type({}): + fail("bun_install: `{}` must be an object".format(field)) + + if field not in ["catalogs", "workspaces.catalogs"]: + return + + for name, catalog in value.items(): + if type(name) != type(""): + fail("bun_install: `catalogs` keys must be strings, got {}".format(type(name))) + if type(catalog) != type({}): + fail("bun_install: `catalogs.{}` must be an object".format(name)) + +def _copy_json_value(value): + return json.decode(json.encode(value)) + +def _normalized_root_manifest(repository_ctx, package_json): + manifest = json.decode(repository_ctx.read(package_json)) + workspaces = manifest.get("workspaces") + + for field in ["catalog", "catalogs"]: + manifest_value = manifest.get(field) + _validate_catalog_shape(field, manifest_value) + + if type(workspaces) != type({}): + continue + + workspace_value = workspaces.get(field) + _validate_catalog_shape("workspaces.{}".format(field), workspace_value) + + if workspace_value == None: + continue + + if manifest_value == None: + manifest[field] = _copy_json_value(workspace_value) + continue + + if manifest_value != workspace_value: + fail( + "bun_install: `{}` conflicts with `workspaces.{}`; use one source of truth or keep both values identical".format(field, field), + ) + + return json.encode(manifest) + def _materialize_workspace_packages(repository_ctx, package_json): package_root = package_json.dirname package_root_str = str(package_root) @@ -187,7 +234,7 @@ def _bun_install_repository_impl(repository_ctx): if lockfile_name not in ["bun.lock", "bun.lockb"]: lockfile_name = "bun.lock" - repository_ctx.file("package.json", repository_ctx.read(package_json)) + repository_ctx.file("package.json", _normalized_root_manifest(repository_ctx, package_json)) repository_ctx.symlink(bun_lockfile, lockfile_name) _materialize_install_inputs(repository_ctx, package_json) _materialize_workspace_packages(repository_ctx, package_json) diff --git a/internal/bun_script.bzl b/internal/bun_script.bzl index fa1d891..0bbc66e 100644 --- a/internal/bun_script.bzl +++ b/internal/bun_script.bzl @@ -66,12 +66,123 @@ runtime_package_dir="${{runtime_workspace}}/${{package_rel_dir}}" mkdir -p "${{runtime_package_dir}}" cp -RL "${{package_dir}}/." "${{runtime_package_dir}}/" +workspace_package_map="${{runtime_workspace}}/workspace-packages.tsv" +python3 - "${{runtime_package_dir}}" >"${{workspace_package_map}}" <<'PY' +import json +import os +import sys + +root = sys.argv[1] + +for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = [name for name in dirnames if name != "node_modules"] + if "package.json" not in filenames: + continue + + manifest_path = os.path.join(dirpath, "package.json") + try: + with open(manifest_path, "r", encoding="utf-8") as manifest_file: + package_name = json.load(manifest_file).get("name") + except Exception: + continue + + if isinstance(package_name, str): + print(f"{{package_name}}\t{{dirpath}}") +PY + install_repo_root="" if [[ -n "${{primary_node_modules}}" ]]; then install_repo_root="$(dirname "${{primary_node_modules}}")" ln -s "${{primary_node_modules}}" "${{runtime_workspace}}/node_modules" fi +workspace_package_dir_for_source() {{ + local source="$1" + local manifest_path="${{source}}/package.json" + local package_name="" + local workspace_dir="" + + if [[ ! -f "${{manifest_path}}" ]]; then + return 1 + fi + + package_name="$(python3 - "${{manifest_path}}" <<'PY' +import json +import sys + +try: + with open(sys.argv[1], "r", encoding="utf-8") as manifest_file: + package_name = json.load(manifest_file).get("name", "") +except Exception: + package_name = "" + +if isinstance(package_name, str): + print(package_name) +PY +)" + + workspace_dir="$(awk -F '\t' -v name="$package_name" '$1 == name {{ print $2; exit }}' "${{workspace_package_map}}")" + if [[ -n "${{package_name}}" && -n "${{workspace_dir}}" ]]; then + echo "${{workspace_dir}}" + return 0 + fi + + return 1 +}} + +link_node_modules_entry() {{ + local source="$1" + local destination="$2" + local workspace_target="" + + rm -rf "${{destination}}" + workspace_target="$(workspace_package_dir_for_source "${{source}}" || true)" + if [[ -n "${{workspace_target}}" ]]; then + ln -s "${{workspace_target}}" "${{destination}}" + return 0 + fi + + if [[ -L "${{source}}" ]]; then + ln -s "$(readlink "${{source}}")" "${{destination}}" + else + ln -s "${{source}}" "${{destination}}" + fi +}} + +mirror_node_modules_dir() {{ + local source_dir="$1" + local destination_dir="$2" + local entry="" + local scoped_entry="" + + rm -rf "${{destination_dir}}" + mkdir -p "${{destination_dir}}" + + shopt -s dotglob nullglob + for entry in "${{source_dir}}"/* "${{source_dir}}"/.[!.]* "${{source_dir}}"/..?*; do + local entry_name="$(basename "${{entry}}")" + if [[ "${{entry_name}}" == "." || "${{entry_name}}" == ".." ]]; then + continue + fi + + if [[ -d "${{entry}}" && ! -L "${{entry}}" && "${{entry_name}}" == @* ]]; then + mkdir -p "${{destination_dir}}/${{entry_name}}" + for scoped_entry in "${{entry}}"/* "${{entry}}"/.[!.]* "${{entry}}"/..?*; do + local scoped_name="$(basename "${{scoped_entry}}")" + if [[ "${{scoped_name}}" == "." || "${{scoped_name}}" == ".." ]]; then + continue + fi + + link_node_modules_entry "${{scoped_entry}}" "${{destination_dir}}/${{entry_name}}/${{scoped_name}}" + done + continue + fi + + link_node_modules_entry "${{entry}}" "${{destination_dir}}/${{entry_name}}" + done + shopt -u dotglob nullglob +}} + find_node_modules() {{ local dir="$1" local root="$2" @@ -118,22 +229,39 @@ find_install_repo_node_modules() {{ return 1 }} +mirror_install_repo_workspace_node_modules() {{ + local repo_root="$1" + local destination_root="$2" + + while IFS= read -r install_node_modules; do + local rel_path="${{install_node_modules#${{repo_root}}/}}" + local destination="${{destination_root}}/${{rel_path}}" + + mkdir -p "$(dirname "${{destination}}")" + mirror_node_modules_dir "${{install_node_modules}}" "${{destination}}" + done < <(find "${{repo_root}}" \ + -path "${{repo_root}}/node_modules" -prune -o \ + -type d -name node_modules -print 2>/dev/null | sort) +}} + resolved_install_node_modules="" if [[ -n "${{install_repo_root}}" ]]; then resolved_install_node_modules="$(find_install_repo_node_modules "${{install_repo_root}}" "${{package_rel_dir}}" || true)" fi if [[ -n "${{resolved_install_node_modules}}" ]]; then - rm -rf "${{runtime_package_dir}}/node_modules" - ln -s "${{resolved_install_node_modules}}" "${{runtime_package_dir}}/node_modules" + mirror_node_modules_dir "${{resolved_install_node_modules}}" "${{runtime_package_dir}}/node_modules" else resolved_node_modules="$(find_node_modules "${{runtime_package_dir}}" "${{runtime_workspace}}" || true)" if [[ -n "${{resolved_node_modules}}" && "${{resolved_node_modules}}" != "${{runtime_package_dir}}/node_modules" ]]; then - rm -rf "${{runtime_package_dir}}/node_modules" - ln -s "${{resolved_node_modules}}" "${{runtime_package_dir}}/node_modules" + mirror_node_modules_dir "${{resolved_node_modules}}" "${{runtime_package_dir}}/node_modules" fi fi +if [[ -n "${{install_repo_root}}" ]]; then + mirror_install_repo_workspace_node_modules "${{install_repo_root}}" "${{runtime_package_dir}}" +fi + path_entries=() if [[ -d "${{runtime_package_dir}}/node_modules/.bin" ]]; then path_entries+=("${{runtime_package_dir}}/node_modules/.bin") @@ -216,4 +344,4 @@ declared in `package.json` and expect to run from the package directory with }, executable = True, toolchains = ["//bun:toolchain_type"], -) \ No newline at end of file +) diff --git a/tests/install_test/BUILD.bazel b/tests/install_test/BUILD.bazel index 4ce9751..c3c47b4 100644 --- a/tests/install_test/BUILD.bazel +++ b/tests/install_test/BUILD.bazel @@ -102,3 +102,22 @@ sh_test( "//conditions:default": ["@bun_linux_x64//:bun"], }), ) + +sh_test( + name = "bun_install_workspaces_catalog_test", + srcs = ["workspaces_catalog.sh"], + args = select({ + ":linux_x86_64": ["$(location @bun_linux_x64//:bun)"], + ":linux_aarch64": ["$(location @bun_linux_aarch64//:bun)"], + ":darwin_x86_64": ["$(location @bun_darwin_x64//:bun)"], + ":darwin_aarch64": ["$(location @bun_darwin_aarch64//:bun)"], + "//conditions:default": ["$(location @bun_linux_x64//:bun)"], + }), + data = select({ + ":linux_x86_64": ["@bun_linux_x64//:bun"], + ":linux_aarch64": ["@bun_linux_aarch64//:bun"], + ":darwin_x86_64": ["@bun_darwin_x64//:bun"], + ":darwin_aarch64": ["@bun_darwin_aarch64//:bun"], + "//conditions:default": ["@bun_linux_x64//:bun"], + }), +) diff --git a/tests/install_test/workspaces_catalog.sh b/tests/install_test/workspaces_catalog.sh new file mode 100755 index 0000000..e3fec05 --- /dev/null +++ b/tests/install_test/workspaces_catalog.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +bun_path="$1" +workdir="$(mktemp -d)" +trap 'rm -rf "${workdir}"' EXIT + +mkdir -p "${workdir}/packages/pkg-a" "${workdir}/packages/pkg-b" "${workdir}/packages/web" + +cat >"${workdir}/package.json" <<'JSON' +{ + "name": "workspace-catalog-root", + "private": true, + "workspaces": { + "packages": ["packages/*"], + "catalog": { + "is-number": "7.0.0", + "vite": "5.4.14" + }, + "catalogs": { + "testing": { + "vitest": "3.2.4" + } + } + } +} +JSON + +cat >"${workdir}/packages/pkg-a/package.json" <<'JSON' +{ + "name": "@workspace/pkg-a", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "is-number": "catalog:" + }, + "scripts": { + "check": "bun -e \"const version = require('is-number/package.json').version; if (version !== '7.0.0') { console.error(version); process.exit(1); }\"" + } +} +JSON + +cat >"${workdir}/packages/pkg-a/index.js" <<'JS' +module.exports = { value: 42 }; +JS + +cat >"${workdir}/packages/pkg-b/package.json" <<'JSON' +{ + "name": "@workspace/pkg-b", + "version": "1.0.0", + "dependencies": { + "@workspace/pkg-a": "workspace:*", + "is-number": "catalog:" + }, + "scripts": { + "check": "bun -e \"const { value } = require('@workspace/pkg-a'); const version = require('is-number/package.json').version; if (value !== 42 || version !== '7.0.0') { console.error({ value, version }); process.exit(1); }\"" + } +} +JSON + +cat >"${workdir}/packages/web/package.json" <<'JSON' +{ + "name": "@workspace/web", + "private": true, + "devDependencies": { + "vite": "catalog:", + "vitest": "catalog:testing" + }, + "scripts": { + "check": "bun -e \"const viteVersion = require('vite/package.json').version; const vitestVersion = require('vitest/package.json').version; if (viteVersion !== '5.4.14' || vitestVersion !== '3.2.4') { console.error({ viteVersion, vitestVersion }); process.exit(1); }\"" + } +} +JSON + +"${bun_path}" install --cwd "${workdir}" >/dev/null +rm -rf "${workdir}/node_modules" "${workdir}/packages/"*/node_modules +"${bun_path}" install --cwd "${workdir}" --frozen-lockfile >/dev/null + +"${bun_path}" run --cwd "${workdir}/packages/pkg-a" check >/dev/null +"${bun_path}" run --cwd "${workdir}/packages/pkg-b" check >/dev/null +"${bun_path}" run --cwd "${workdir}/packages/web" check >/dev/null diff --git a/tests/script_test/BUILD.bazel b/tests/script_test/BUILD.bazel index 7c6550b..b17d41a 100644 --- a/tests/script_test/BUILD.bazel +++ b/tests/script_test/BUILD.bazel @@ -83,4 +83,63 @@ sh_test( ":vite_monorepo_app_a_dev_server", ":vite_monorepo_app_b_dev_server", ], -) \ No newline at end of file +) + +bun_script( + name = "paraglide_monorepo_app_a_build", + script = "build:app-a", + package_json = "paraglide_monorepo/package.json", + node_modules = "@script_test_paraglide_monorepo_node_modules//:node_modules", + data = [ + "paraglide_monorepo/scripts/build-app-a.mjs", + "paraglide_monorepo/scripts/build-app-b.mjs", + "paraglide_monorepo/packages/i18n/package.json", + "paraglide_monorepo/packages/i18n/project.inlang/settings.json", + "paraglide_monorepo/packages/i18n/messages/en.json", + "paraglide_monorepo/packages/i18n/messages/sv.json", + "paraglide_monorepo/packages/app-a/package.json", + "paraglide_monorepo/packages/app-a/index.html", + "paraglide_monorepo/packages/app-a/main.js", + "paraglide_monorepo/packages/app-a/vite.config.js", + "paraglide_monorepo/packages/app-b/package.json", + "paraglide_monorepo/packages/app-b/index.html", + "paraglide_monorepo/packages/app-b/main.js", + "paraglide_monorepo/packages/app-b/vite.config.js", + ], +) + +bun_script( + name = "paraglide_monorepo_app_b_build", + script = "build:app-b", + package_json = "paraglide_monorepo/package.json", + node_modules = "@script_test_paraglide_monorepo_node_modules//:node_modules", + data = [ + "paraglide_monorepo/scripts/build-app-a.mjs", + "paraglide_monorepo/scripts/build-app-b.mjs", + "paraglide_monorepo/packages/i18n/package.json", + "paraglide_monorepo/packages/i18n/project.inlang/settings.json", + "paraglide_monorepo/packages/i18n/messages/en.json", + "paraglide_monorepo/packages/i18n/messages/sv.json", + "paraglide_monorepo/packages/app-a/package.json", + "paraglide_monorepo/packages/app-a/index.html", + "paraglide_monorepo/packages/app-a/main.js", + "paraglide_monorepo/packages/app-a/vite.config.js", + "paraglide_monorepo/packages/app-b/package.json", + "paraglide_monorepo/packages/app-b/index.html", + "paraglide_monorepo/packages/app-b/main.js", + "paraglide_monorepo/packages/app-b/vite.config.js", + ], +) + +sh_test( + name = "bun_script_paraglide_monorepo_build_test", + srcs = ["run_paraglide_monorepo_builds.sh"], + args = [ + "$(location :paraglide_monorepo_app_a_build)", + "$(location :paraglide_monorepo_app_b_build)", + ], + data = [ + ":paraglide_monorepo_app_a_build", + ":paraglide_monorepo_app_b_build", + ], +) diff --git a/tests/script_test/paraglide_monorepo/bun.lock b/tests/script_test/paraglide_monorepo/bun.lock new file mode 100644 index 0000000..8202199 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/bun.lock @@ -0,0 +1,218 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "paraglide-monorepo-test", + }, + "packages/app-a": { + "name": "paraglide-monorepo-app-a", + "dependencies": { + "@workspace/i18n": "workspace:*", + }, + "devDependencies": { + "vite": "catalog:", + }, + }, + "packages/app-b": { + "name": "paraglide-monorepo-app-b", + "dependencies": { + "@workspace/i18n": "workspace:*", + }, + "devDependencies": { + "vite": "catalog:", + }, + }, + "packages/i18n": { + "name": "@workspace/i18n", + "devDependencies": { + "@inlang/paraglide-js": "catalog:", + }, + }, + }, + "catalog": { + "@inlang/paraglide-js": "2.13.2", + "vite": "5.4.14", + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@inlang/paraglide-js": ["@inlang/paraglide-js@2.13.2", "", { "dependencies": { "@inlang/recommend-sherlock": "^0.2.1", "@inlang/sdk": "^2.7.0", "commander": "11.1.0", "consola": "3.4.0", "json5": "2.2.3", "unplugin": "^2.1.2", "urlpattern-polyfill": "^10.0.0" }, "bin": { "paraglide-js": "bin/run.js" } }, "sha512-ecxw95pmMbasVj7M/B6pu5wqYHomYQBcu3QzDl1svwAkbnRqRmsdrH4IizzFwqeVWd+uluibMIy1VOGywin94A=="], + + "@inlang/recommend-sherlock": ["@inlang/recommend-sherlock@0.2.1", "", { "dependencies": { "comment-json": "^4.2.3" } }, "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg=="], + + "@inlang/sdk": ["@inlang/sdk@2.7.0", "", { "dependencies": { "@lix-js/sdk": "0.4.7", "@sinclair/typebox": "^0.31.17", "kysely": "^0.27.4", "sqlite-wasm-kysely": "0.3.0", "uuid": "^13.0.0" } }, "sha512-yJNBD0o8i29TTJqWX5uDRHxnalDGcsUDctxepzFXsUfkzqGWfiFBxODdxvReqvM2CuKAAOo/kib/F1UcgdYFNQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@lix-js/sdk": ["@lix-js/sdk@0.4.7", "", { "dependencies": { "@lix-js/server-protocol-schema": "0.1.1", "dedent": "1.5.1", "human-id": "^4.1.1", "js-sha256": "^0.11.0", "kysely": "^0.27.4", "sqlite-wasm-kysely": "0.3.0", "uuid": "^10.0.0" } }, "sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ=="], + + "@lix-js/server-protocol-schema": ["@lix-js/server-protocol-schema@0.1.1", "", {}, "sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.31.28", "", {}, "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ=="], + + "@sqlite.org/sqlite-wasm": ["@sqlite.org/sqlite-wasm@3.48.0-build4", "", { "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@workspace/i18n": ["@workspace/i18n@workspace:packages/i18n"], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="], + + "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "comment-json": ["comment-json@4.6.2", "", { "dependencies": { "array-timsort": "^1.0.3", "esprima": "^4.0.1" } }, "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w=="], + + "consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="], + + "dedent": ["dedent@1.5.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="], + + "js-sha256": ["js-sha256@0.11.1", "", {}, "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "kysely": ["kysely@0.27.6", "", {}, "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "paraglide-monorepo-app-a": ["paraglide-monorepo-app-a@workspace:packages/app-a"], + + "paraglide-monorepo-app-b": ["paraglide-monorepo-app-b@workspace:packages/app-b"], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "sqlite-wasm-kysely": ["sqlite-wasm-kysely@0.3.0", "", { "dependencies": { "@sqlite.org/sqlite-wasm": "^3.48.0-build2" }, "peerDependencies": { "kysely": "*" } }, "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg=="], + + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + + "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], + + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + + "vite": ["vite@5.4.14", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA=="], + + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + + "@lix-js/sdk/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + } +} diff --git a/tests/script_test/paraglide_monorepo/package.json b/tests/script_test/paraglide_monorepo/package.json new file mode 100644 index 0000000..72a8295 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/package.json @@ -0,0 +1,17 @@ +{ + "name": "paraglide-monorepo-test", + "private": true, + "workspaces": { + "packages": [ + "packages/*" + ], + "catalog": { + "@inlang/paraglide-js": "2.13.2", + "vite": "5.4.14" + } + }, + "scripts": { + "build:app-a": "bun ./scripts/build-app-a.mjs", + "build:app-b": "bun ./scripts/build-app-b.mjs" + } +} diff --git a/tests/script_test/paraglide_monorepo/packages/app-a/index.html b/tests/script_test/paraglide_monorepo/packages/app-a/index.html new file mode 100644 index 0000000..a5bf961 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/app-a/index.html @@ -0,0 +1,12 @@ + + + + + + Paraglide monorepo app A + + +
+ + + diff --git a/tests/script_test/paraglide_monorepo/packages/app-a/main.js b/tests/script_test/paraglide_monorepo/packages/app-a/main.js new file mode 100644 index 0000000..01699f6 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/app-a/main.js @@ -0,0 +1,9 @@ +import * as messages from "@workspace/i18n/messages"; +import { setLocale } from "@workspace/i18n/runtime"; + +setLocale("sv"); + +const app = document.querySelector("#app"); +if (app) { + app.textContent = `${messages.hero()} :: ${messages.hello()}`; +} diff --git a/tests/script_test/paraglide_monorepo/packages/app-a/package.json b/tests/script_test/paraglide_monorepo/packages/app-a/package.json new file mode 100644 index 0000000..26f043d --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/app-a/package.json @@ -0,0 +1,14 @@ +{ + "name": "paraglide-monorepo-app-a", + "private": true, + "type": "module", + "scripts": { + "build": "vite build" + }, + "dependencies": { + "@workspace/i18n": "workspace:*" + }, + "devDependencies": { + "vite": "catalog:" + } +} diff --git a/tests/script_test/paraglide_monorepo/packages/app-a/vite.config.js b/tests/script_test/paraglide_monorepo/packages/app-a/vite.config.js new file mode 100644 index 0000000..0e665f3 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/app-a/vite.config.js @@ -0,0 +1,10 @@ +export default { + resolve: { + preserveSymlinks: true, + }, + optimizeDeps: { + esbuildOptions: { + preserveSymlinks: true, + }, + }, +}; diff --git a/tests/script_test/paraglide_monorepo/packages/app-b/index.html b/tests/script_test/paraglide_monorepo/packages/app-b/index.html new file mode 100644 index 0000000..1070527 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/app-b/index.html @@ -0,0 +1,12 @@ + + + + + + Paraglide monorepo app B + + +
+ + + diff --git a/tests/script_test/paraglide_monorepo/packages/app-b/main.js b/tests/script_test/paraglide_monorepo/packages/app-b/main.js new file mode 100644 index 0000000..09ab6e4 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/app-b/main.js @@ -0,0 +1,6 @@ +import * as messages from "@workspace/i18n/messages"; + +const app = document.querySelector("#app"); +if (app) { + app.textContent = `${messages.hero({ locale: "en" })} :: ${messages.hello({ locale: "en" })}`; +} diff --git a/tests/script_test/paraglide_monorepo/packages/app-b/package.json b/tests/script_test/paraglide_monorepo/packages/app-b/package.json new file mode 100644 index 0000000..0d62b6c --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/app-b/package.json @@ -0,0 +1,14 @@ +{ + "name": "paraglide-monorepo-app-b", + "private": true, + "type": "module", + "scripts": { + "build": "vite build" + }, + "dependencies": { + "@workspace/i18n": "workspace:*" + }, + "devDependencies": { + "vite": "catalog:" + } +} diff --git a/tests/script_test/paraglide_monorepo/packages/app-b/vite.config.js b/tests/script_test/paraglide_monorepo/packages/app-b/vite.config.js new file mode 100644 index 0000000..0e665f3 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/app-b/vite.config.js @@ -0,0 +1,10 @@ +export default { + resolve: { + preserveSymlinks: true, + }, + optimizeDeps: { + esbuildOptions: { + preserveSymlinks: true, + }, + }, +}; diff --git a/tests/script_test/paraglide_monorepo/packages/i18n/messages/en.json b/tests/script_test/paraglide_monorepo/packages/i18n/messages/en.json new file mode 100644 index 0000000..2318743 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/i18n/messages/en.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "hello": "Hello from the shared translations", + "hero": "One shared translation source" +} diff --git a/tests/script_test/paraglide_monorepo/packages/i18n/messages/sv.json b/tests/script_test/paraglide_monorepo/packages/i18n/messages/sv.json new file mode 100644 index 0000000..9242955 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/i18n/messages/sv.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "hello": "Hej fran de delade oversattningarna", + "hero": "En gemensam oversattningskalla" +} diff --git a/tests/script_test/paraglide_monorepo/packages/i18n/package.json b/tests/script_test/paraglide_monorepo/packages/i18n/package.json new file mode 100644 index 0000000..36ce75c --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/i18n/package.json @@ -0,0 +1,15 @@ +{ + "name": "@workspace/i18n", + "private": true, + "type": "module", + "exports": { + "./messages": "./src/paraglide/messages.js", + "./runtime": "./src/paraglide/runtime.js" + }, + "scripts": { + "build": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide" + }, + "devDependencies": { + "@inlang/paraglide-js": "catalog:" + } +} diff --git a/tests/script_test/paraglide_monorepo/packages/i18n/project.inlang/settings.json b/tests/script_test/paraglide_monorepo/packages/i18n/project.inlang/settings.json new file mode 100644 index 0000000..ac9cde7 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/packages/i18n/project.inlang/settings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "baseLocale": "en", + "locales": [ + "en", + "sv" + ], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + } +} diff --git a/tests/script_test/paraglide_monorepo/scripts/build-app-a.mjs b/tests/script_test/paraglide_monorepo/scripts/build-app-a.mjs new file mode 100644 index 0000000..aae97b9 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/scripts/build-app-a.mjs @@ -0,0 +1,14 @@ +import { spawnSync } from "node:child_process"; + +const bun = process.execPath; +const extraArgs = process.argv.slice(2); + +for (const args of [ + ["run", "--cwd", "./packages/i18n", "build"], + ["run", "--cwd", "./packages/app-a", "build", "--", ...extraArgs], +]) { + const result = spawnSync(bun, args, { stdio: "inherit" }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} diff --git a/tests/script_test/paraglide_monorepo/scripts/build-app-b.mjs b/tests/script_test/paraglide_monorepo/scripts/build-app-b.mjs new file mode 100644 index 0000000..34eac49 --- /dev/null +++ b/tests/script_test/paraglide_monorepo/scripts/build-app-b.mjs @@ -0,0 +1,14 @@ +import { spawnSync } from "node:child_process"; + +const bun = process.execPath; +const extraArgs = process.argv.slice(2); + +for (const args of [ + ["run", "--cwd", "./packages/i18n", "build"], + ["run", "--cwd", "./packages/app-b", "build", "--", ...extraArgs], +]) { + const result = spawnSync(bun, args, { stdio: "inherit" }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} diff --git a/tests/script_test/run_paraglide_monorepo_builds.sh b/tests/script_test/run_paraglide_monorepo_builds.sh new file mode 100755 index 0000000..a304162 --- /dev/null +++ b/tests/script_test/run_paraglide_monorepo_builds.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +app_a_binary="$1" +app_b_binary="$2" +workdir="$(mktemp -d)" + +cleanup() { + rm -rf "${workdir}" +} +trap cleanup EXIT + +verify_build() { + local binary="$1" + local out_dir="$2" + local expected_title="$3" + local expected_text="$4" + + "${binary}" --outDir "${out_dir}" >/dev/null + + if [[ ! -f "${out_dir}/index.html" ]]; then + echo "missing build output index.html for ${binary}" >&2 + exit 1 + fi + + if ! grep -Fq "${expected_title}" "${out_dir}/index.html"; then + echo "missing expected title in ${out_dir}/index.html" >&2 + exit 1 + fi + + local asset + asset="$(find "${out_dir}/assets" -type f -name '*.js' | head -n 1)" + if [[ -z ${asset} ]]; then + echo "missing built JS asset for ${binary}" >&2 + exit 1 + fi + + if ! grep -Fq "${expected_text}" "${asset}"; then + echo "missing expected translated text in ${asset}" >&2 + exit 1 + fi +} + +verify_build \ + "${app_a_binary}" \ + "${workdir}/app-a-dist" \ + "Paraglide monorepo app A" \ + "En gemensam oversattningskalla" + +verify_build \ + "${app_b_binary}" \ + "${workdir}/app-b-dist" \ + "Paraglide monorepo app B" \ + "One shared translation source" From c446f23a354cc02d73cedec6e27470d2277f2b7b Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 14 Mar 2026 23:50:26 +0100 Subject: [PATCH 2/4] feat: improve rules_js parity --- MODULE.bazel.lock | 2 +- README.md | 33 + bun/BUILD.bazel | 1 + bun/defs.bzl | 7 +- bun/extensions.bzl | 1 + docs/BUILD.bazel | 3 + docs/bun_install.md | 1 + docs/rules.md | 47 ++ flake.lock | 80 +-- flake.nix | 200 +++--- internal/BUILD.bazel | 28 + internal/bun_binary.bzl | 83 +-- internal/bun_bundle.bzl | 7 +- internal/bun_dev.bzl | 158 ++--- internal/bun_install.bzl | 133 +++- internal/bun_script.bzl | 304 +------- internal/bun_test.bzl | 82 ++- internal/js_compat.bzl | 29 + internal/js_library.bzl | 75 +- internal/js_run_devserver.bzl | 100 +++ internal/workspace.bzl | 649 ++++++++++++++++++ js/BUILD.bazel | 12 + js/defs.bzl | 12 + npm/BUILD.bazel | 22 + npm/extensions.bzl | 28 + npm/repositories.bzl | 11 + tests/install_extension_test/BUILD.bazel | 7 + .../npm_extension_shape_test.sh | 10 + tests/js_compat_test/BUILD.bazel | 41 ++ tests/js_compat_test/app.test.ts | 7 + tests/js_compat_test/helper.ts | 6 + tests/js_compat_test/main.ts | 3 + tests/js_compat_test/payload.txt | 1 + tests/js_compat_test/run_binary.sh | 10 + tests/js_compat_test/run_devserver.sh | 10 + .../npm_translate_lock_workspace_test.sh | 119 ++++ 36 files changed, 1683 insertions(+), 639 deletions(-) create mode 100644 internal/js_compat.bzl create mode 100644 internal/js_run_devserver.bzl create mode 100644 internal/workspace.bzl create mode 100644 js/BUILD.bazel create mode 100644 js/defs.bzl create mode 100644 npm/BUILD.bazel create mode 100644 npm/extensions.bzl create mode 100644 npm/repositories.bzl create mode 100755 tests/install_extension_test/npm_extension_shape_test.sh create mode 100644 tests/js_compat_test/BUILD.bazel create mode 100644 tests/js_compat_test/app.test.ts create mode 100644 tests/js_compat_test/helper.ts create mode 100644 tests/js_compat_test/main.ts create mode 100644 tests/js_compat_test/payload.txt create mode 100755 tests/js_compat_test/run_binary.sh create mode 100755 tests/js_compat_test/run_devserver.sh create mode 100755 tests/npm_compat_test/npm_translate_lock_workspace_test.sh diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 8c3b8c2..41c5815 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -192,7 +192,7 @@ "moduleExtensions": { "//bun:extensions.bzl%bun": { "general": { - "bzlTransitiveDigest": "eSFVebwDN61an1dp3505njvMKN961HH+iY2tK6fEBQQ=", + "bzlTransitiveDigest": "mWoMIEcKvXURFuMv68yk2TPrpNykLSLygedENme3WrQ=", "usagesDigest": "/0BcCMA6AOzLhQaRK6DquxrCfpPHJUjSUaFz4zmQrsM=", "recordedInputs": [ "REPO_MAPPING:,bazel_tools bazel_tools" diff --git a/README.md b/README.md index 49ef567..12cdaaf 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ The public entrypoint for rule authors and users is `@rules_bun//bun:defs.bzl`. - `bun_dev` - `bun_script` - `bun_test` +- `js_binary` +- `js_test` +- `js_run_devserver` - `js_library` - `ts_library` @@ -105,6 +108,36 @@ bun_install_ext.install( use_repo(bun_install_ext, "bun_deps") ``` +## `rules_js` compatibility layer + +`rules_bun` now exposes a Bun-backed compatibility layer for the most common +`rules_js` entrypoints: + +- `@rules_bun//js:defs.bzl` exports `js_binary`, `js_test`, `js_run_devserver`, + `js_library`, `ts_library`, and `JsInfo`. +- `@rules_bun//npm:extensions.bzl` exports `npm_translate_lock`, which creates a + Bun-installed external repo and generates `@//:defs.bzl` with + `npm_link_all_packages()`. + +Example: + +```starlark +load("@rules_bun//js:defs.bzl", "js_binary") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +js_binary( + name = "app", + entry_point = "src/main.ts", + node_modules = ":node_modules", +) +``` + +This is a compatibility subset, not a full reimplementation of `rules_js`. +Package aliases created by `npm_link_all_packages()` use sanitized target names +such as `npm__vite` or `npm__at_types_node`. + ## Legacy WORKSPACE usage For non-Bzlmod consumers, the repository exposes a legacy setup macro in diff --git a/bun/BUILD.bazel b/bun/BUILD.bazel index 62bf0a1..645c682 100644 --- a/bun/BUILD.bazel +++ b/bun/BUILD.bazel @@ -40,6 +40,7 @@ bzl_library( "//internal:bun_dev_bzl", "//internal:bun_script_bzl", "//internal:bun_test_bzl", + "//internal:js_compat_bzl", "//internal:js_library_bzl", ], ) diff --git a/bun/defs.bzl b/bun/defs.bzl index 104f73a..4c0ccc7 100644 --- a/bun/defs.bzl +++ b/bun/defs.bzl @@ -4,6 +4,7 @@ load("//internal:bun_bundle.bzl", _bun_bundle = "bun_bundle") load("//internal:bun_dev.bzl", _bun_dev = "bun_dev") load("//internal:bun_script.bzl", _bun_script = "bun_script") load("//internal:bun_test.bzl", _bun_test = "bun_test") +load("//internal:js_compat.bzl", _JsInfo = "JsInfo", _js_binary = "js_binary", _js_run_devserver = "js_run_devserver", _js_test = "js_test") load("//internal:js_library.bzl", _js_library = "js_library", _ts_library = "ts_library") load(":toolchain.bzl", _BunToolchainInfo = "BunToolchainInfo", _bun_toolchain = "bun_toolchain") @@ -14,8 +15,12 @@ bun_bundle = _bun_bundle bun_dev = _bun_dev bun_script = _bun_script bun_test = _bun_test +js_binary = _js_binary +js_test = _js_test +js_run_devserver = _js_run_devserver js_library = _js_library ts_library = _ts_library +JsInfo = _JsInfo BunToolchainInfo = _BunToolchainInfo bun_toolchain = _bun_toolchain - \ No newline at end of file + diff --git a/bun/extensions.bzl b/bun/extensions.bzl index ea012cc..1b7f9b4 100644 --- a/bun/extensions.bzl +++ b/bun/extensions.bzl @@ -75,6 +75,7 @@ def _bun_install_impl(ctx): bun_lockfile = install.bun_lockfile, install_inputs = install.install_inputs, isolated_home = install.isolated_home, + visible_repo_name = install.name, ) diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 5614f36..76dccf4 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -12,6 +12,9 @@ stardoc( "bun_dev", "bun_script", "bun_test", + "js_binary", + "js_run_devserver", + "js_test", "js_library", "ts_library", ], diff --git a/docs/bun_install.md b/docs/bun_install.md index d4e6961..4d2dab0 100644 --- a/docs/bun_install.md +++ b/docs/bun_install.md @@ -14,6 +14,7 @@ Unlike the build rules in [rules.md](rules.md), `bun_install` is not loaded from - runs `bun install --frozen-lockfile` - uses your checked-in `package.json` and `bun.lock` or `bun.lockb` - creates an external Bazel repository exposing `:node_modules` +- generates `:defs.bzl` with `npm_link_all_packages()` and `package_target_name()` - keeps dependency installation under Bun rather than npm The generated repository can then be passed to rules such as `bun_script`, diff --git a/docs/rules.md b/docs/rules.md index 3457715..8aaf823 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -2,6 +2,46 @@ This file documents the public rules exported from `@rules_bun//bun:defs.bzl`. +## js_binary + +Runs a JS/TS entry point with Bun behind a `rules_js`-style name. + +Attributes: + +- `entry_point` (label, required): path to the main JS/TS file to execute. +- `node_modules` (label, optional): package files from a `node_modules` tree, typically produced by `bun_install` or `npm_translate_lock`, made available in runfiles. +- `data` (label_list, optional): additional runtime files. +- `deps` (label_list, optional): library dependencies required by the program. +- `args` (string_list, optional): default arguments appended before command-line arguments passed to the binary. +- `working_dir` (string, default: `"workspace"`, values: `"workspace" | "entry_point"`): runtime working directory. + +## js_test + +Runs Bun tests behind a `rules_js`-style name. + +Attributes: + +- `srcs` (label_list, required): test source files passed to `bun test`. +- `node_modules` (label, optional): package files from a `node_modules` tree, typically produced by `bun_install` or `npm_translate_lock`, made available in runfiles. +- `deps` (label_list, optional): library dependencies required by tests. +- `data` (label_list, optional): additional runtime files needed by tests. +- `args` (string_list, optional): default arguments appended after the test source list. + +## js_run_devserver + +Runs an executable target from a staged JS workspace. + +Attributes: + +- `tool` (label, required): executable target to launch as the dev server. +- `args` (string_list, optional): default arguments appended before command-line arguments passed to the dev server. +- `package_json` (label, optional): package manifest used to resolve the package working directory. +- `package_dir_hint` (string, default: `"."`): package-relative directory hint when `package_json` is omitted. +- `node_modules` (label, optional): package files from a `node_modules` tree, typically produced by `bun_install` or `npm_translate_lock`, made available in runfiles. +- `deps` (label_list, optional): library dependencies required by the dev server. +- `data` (label_list, optional): additional runtime files. +- `working_dir` (string, default: `"workspace"`, values: `"workspace" | "package"`): runtime working directory. + ## bun_binary Runs a JS/TS entry point with Bun as an executable target (`bazel run`). @@ -11,6 +51,8 @@ Attributes: - `entry_point` (label, required): path to the main JS/TS file to execute. - `node_modules` (label, optional): package files from a `node_modules` tree, typically produced by `bun_install`, made available in runfiles. - `data` (label_list, optional): additional runtime files. +- `deps` (label_list, optional): library dependencies required by the program. +- `args` (string_list, optional): default arguments appended before command-line arguments passed to the binary. - `working_dir` (string, default: `"workspace"`, values: `"workspace" | "entry_point"`): runtime working directory. ## bun_dev @@ -68,6 +110,7 @@ Attributes: - `node_modules` (label, optional): package files from a `node_modules` tree, typically produced by `bun_install`, made available in runfiles. - `deps` (label_list, optional): library dependencies required by tests. - `data` (label_list, optional): additional runtime files needed by tests. +- `args` (string_list, optional): default arguments appended after the test source list. ## js_library @@ -76,6 +119,8 @@ Aggregates JavaScript sources and transitive Bun source dependencies. Attributes: - `srcs` (label_list, optional): `.js`, `.jsx`, `.mjs`, `.cjs` files. +- `types` (label_list, optional): `.d.ts` files propagated to dependents. +- `data` (label_list, optional): runtime files propagated to dependents. - `deps` (label_list, optional): dependent source libraries. ## ts_library @@ -85,4 +130,6 @@ Aggregates TypeScript sources and transitive Bun source dependencies. Attributes: - `srcs` (label_list, optional): `.ts`, `.tsx` files. +- `types` (label_list, optional): `.d.ts` files propagated to dependents. +- `data` (label_list, optional): runtime files propagated to dependents. - `deps` (label_list, optional): dependent source libraries. diff --git a/flake.lock b/flake.lock index 219148f..bed0801 100644 --- a/flake.lock +++ b/flake.lock @@ -1,28 +1,5 @@ { "nodes": { - "devshell-lib": { - "inputs": { - "git-hooks": "git-hooks", - "nixpkgs": [ - "nixpkgs" - ], - "treefmt-nix": "treefmt-nix" - }, - "locked": { - "lastModified": 1772815059, - "narHash": "sha256-9Mn8t/a7b43omtmKRsF0HmFpCkNpTsvYEq0y85KLL5s=", - "ref": "v2.0.1", - "rev": "80cc529de7060e079d89a69d8daaf0347b53d8f9", - "revCount": 43, - "type": "git", - "url": "https://git.dgren.dev/eric/nix-flake-lib" - }, - "original": { - "ref": "v2.0.1", - "type": "git", - "url": "https://git.dgren.dev/eric/nix-flake-lib" - } - }, "flake-compat": { "flake": false, "locked": { @@ -43,7 +20,7 @@ "inputs": { "flake-compat": "flake-compat", "gitignore": "gitignore", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs_2" }, "locked": { "lastModified": 1772024342, @@ -62,7 +39,7 @@ "gitignore": { "inputs": { "nixpkgs": [ - "devshell-lib", + "repo-lib", "git-hooks", "nixpkgs" ] @@ -82,6 +59,22 @@ } }, "nixpkgs": { + "locked": { + "lastModified": 1772542754, + "narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { "locked": { "lastModified": 1770073757, "narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=", @@ -97,7 +90,7 @@ "type": "github" } }, - "nixpkgs_2": { + "nixpkgs_3": { "locked": { "lastModified": 1770107345, "narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=", @@ -113,31 +106,38 @@ "type": "github" } }, - "nixpkgs_3": { + "repo-lib": { + "inputs": { + "git-hooks": "git-hooks", + "nixpkgs": [ + "nixpkgs" + ], + "treefmt-nix": "treefmt-nix" + }, "locked": { - "lastModified": 1772542754, - "narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4", - "type": "github" + "lastModified": 1772866275, + "narHash": "sha256-lsJrFIbq6OO5wUC648VnvOmJm3qgJrlEugbdjeZsP34=", + "ref": "refs/tags/v3.0.0", + "rev": "96d2d190466dddcb9e652c38b70152f09b9fcb05", + "revCount": 50, + "type": "git", + "url": "https://git.dgren.dev/eric/nix-flake-lib" }, "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" + "ref": "refs/tags/v3.0.0", + "type": "git", + "url": "https://git.dgren.dev/eric/nix-flake-lib" } }, "root": { "inputs": { - "devshell-lib": "devshell-lib", - "nixpkgs": "nixpkgs_3" + "nixpkgs": "nixpkgs", + "repo-lib": "repo-lib" } }, "treefmt-nix": { "inputs": { - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_3" }, "locked": { "lastModified": 1770228511, diff --git a/flake.nix b/flake.nix index 97ca90b..5b35a56 100644 --- a/flake.nix +++ b/flake.nix @@ -3,143 +3,50 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; - devshell-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=v2.0.1"; - devshell-lib.inputs.nixpkgs.follows = "nixpkgs"; + repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.0.0"; + repo-lib.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = { self, nixpkgs, - devshell-lib, + repo-lib, ... }: let - supportedSystems = [ - "x86_64-linux" - "aarch64-linux" - "x86_64-darwin" - "aarch64-darwin" - ]; - forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + bazelVersion = "9.0.0"; in - { - devShells = forAllSystems ( - system: - let - pkgs = import nixpkgs { inherit system; }; - bazel9 = pkgs.writeShellScriptBin "bazel" '' - export USE_BAZEL_VERSION="''${USE_BAZEL_VERSION:-9.0.0}" - exec ${pkgs.bazelisk}/bin/bazelisk "$@" - ''; - env = devshell-lib.lib.mkDevShell { - inherit system; + repo-lib.lib.mkRepo { + inherit self nixpkgs; + src = ./.; - extraPackages = with pkgs; [ - go - gopls - gotools - bun - bazel9 - bazel-buildtools - self.packages.${system}.release - ]; + config = { + shell.extraShellText = '' + export USE_BAZEL_VERSION="''${USE_BAZEL_VERSION:-${bazelVersion}}" + export BUN_INSTALL="''${BUN_INSTALL:-$HOME/.bun}" + export PATH="$BUN_INSTALL/bin:$PATH" + ''; - features = { - oxfmt = false; - }; + formatting = { + programs.shfmt.enable = true; + settings.shfmt.options = [ + "-i" + "2" + "-s" + "-w" + ]; + }; - formatters = { - shfmt.enable = true; - }; - - formatterSettings = { - shfmt.options = [ - "-i" - "2" - "-s" - "-w" - ]; - }; - - additionalHooks = { - tests = { - enable = true; - entry = '' - bazel test //tests/... - ''; - pass_filenames = false; - stages = [ "pre-push" ]; - }; - }; - - tools = [ - { - name = "Bun"; - bin = "${pkgs.bun}/bin/bun"; - versionCmd = "--version"; - color = "YELLOW"; - } - { - name = "Go"; - bin = "${pkgs.go}/bin/go"; - versionCmd = "version"; - color = "CYAN"; - } - { - name = "Bazel"; - bin = "${bazel9}/bin/bazel"; - versionCmd = "--version"; - color = "GREEN"; - } - ]; - - extraShellHook = '' - export USE_BAZEL_VERSION="''${USE_BAZEL_VERSION:-9.0.0}" - export BUN_INSTALL="''${BUN_INSTALL:-$HOME/.bun}" - export PATH="$BUN_INSTALL/bin:$PATH" - ''; - }; - in - { - default = env.shell; - } - ); - - checks = forAllSystems ( - system: - let - env = devshell-lib.lib.mkDevShell { inherit system; }; - in - { - inherit (env) pre-commit-check; - } - ); - - formatter = forAllSystems (system: (devshell-lib.lib.mkDevShell { inherit system; }).formatter); - - # Optional: release command (`release`) - # - # The release script always updates VERSION first, then: - # 1) runs release steps in order (file writes and scripts) - # 2) runs postVersion hook - # 3) formats, stages, commits, tags, and pushes - # - # Runtime env vars available in release.run/postVersion: - # BASE_VERSION, CHANNEL, PRERELEASE_NUM, FULL_VERSION, FULL_TAG - # - packages = forAllSystems (system: { - release = devshell-lib.lib.mkRelease { - inherit system; - - release = [ + release = { + steps = [ { - run = '' + run.script = '' sed -E -i 's#^([[:space:]]*version[[:space:]]*=[[:space:]]*")[^"]*(",)$#\1'"$FULL_VERSION"'\2#' "$ROOT_DIR/MODULE.bazel" ''; } { - run = '' + run.script = '' README="$ROOT_DIR/README.md" TMP="$README.tmp" @@ -174,8 +81,59 @@ echo "Released $FULL_TAG" ''; }; - }); + }; + perSystem = + { + pkgs, + system, + ... + }: + let + bazel9 = pkgs.writeShellScriptBin "bazel" '' + export USE_BAZEL_VERSION="''${USE_BAZEL_VERSION:-${bazelVersion}}" + exec ${pkgs.bazelisk}/bin/bazelisk "$@" + ''; + in + { + tools = [ + (repo-lib.lib.tools.fromPackage { + name = "Bun"; + package = pkgs.bun; + version.args = [ "--version" ]; + banner.color = "YELLOW"; + }) + (repo-lib.lib.tools.fromPackage { + name = "Go"; + package = pkgs.go; + version.args = [ "version" ]; + banner.color = "CYAN"; + }) + (repo-lib.lib.tools.fromPackage { + name = "Bazel"; + package = bazel9; + version.args = [ "--version" ]; + banner.color = "GREEN"; + }) + ]; + + shell.packages = [ + pkgs.gopls + pkgs.gotools + pkgs.bazel-buildtools + self.packages.${system}.release + ]; + + checks.tests = { + command = "bazel test //tests/..."; + stage = "pre-push"; + passFilenames = false; + runtimeInputs = [ + bazel9 + pkgs.bun + pkgs.go + ]; + }; + }; }; - } diff --git a/internal/BUILD.bazel b/internal/BUILD.bazel index c5ee197..c77c112 100644 --- a/internal/BUILD.bazel +++ b/internal/BUILD.bazel @@ -9,7 +9,10 @@ exports_files([ "bun_install.bzl", "bun_script.bzl", "bun_test.bzl", + "js_compat.bzl", "js_library.bzl", + "js_run_devserver.bzl", + "workspace.bzl", ]) bzl_library( @@ -44,7 +47,32 @@ bzl_library( deps = [":js_library_bzl"], ) +bzl_library( + name = "js_compat_bzl", + srcs = ["js_compat.bzl"], + deps = [ + ":bun_binary_bzl", + ":bun_test_bzl", + ":js_library_bzl", + ":js_run_devserver_bzl", + ], +) + bzl_library( name = "js_library_bzl", srcs = ["js_library.bzl"], ) + +bzl_library( + name = "js_run_devserver_bzl", + srcs = ["js_run_devserver.bzl"], + deps = [ + ":js_library_bzl", + ":workspace_bzl", + ], +) + +bzl_library( + name = "workspace_bzl", + srcs = ["workspace.bzl"], +) diff --git a/internal/bun_binary.bzl b/internal/bun_binary.bzl index 52daa84..d447789 100644 --- a/internal/bun_binary.bzl +++ b/internal/bun_binary.bzl @@ -1,67 +1,55 @@ """Rule for running JS/TS scripts with Bun.""" +load("//internal:js_library.bzl", "collect_js_runfiles") +load("//internal:workspace.bzl", "create_bun_workspace_info", "render_workspace_setup", "workspace_runfiles") + +def _shell_quote(value): + return "'" + value.replace("'", "'\"'\"'") + "'" def _bun_binary_impl(ctx): toolchain = ctx.toolchains["//bun:toolchain_type"] bun_bin = toolchain.bun.bun_bin entry_point = ctx.file.entry_point + dep_runfiles = [collect_js_runfiles(dep) for dep in ctx.attr.deps] + workspace_info = create_bun_workspace_info( + ctx, + extra_files = ctx.files.data + [bun_bin], + primary_file = entry_point, + ) + + command = """ +trap cleanup_runtime_workspace EXIT +cd "${runtime_exec_dir}" +exec "${bun_bin}" --bun run "${primary_source}" "$@" +""" + if ctx.attr.args: + command = """ +trap cleanup_runtime_workspace EXIT +cd "${runtime_exec_dir}" +exec "${bun_bin}" --bun run "${primary_source}" __DEFAULT_ARGS__ "$@" +""".replace("__DEFAULT_ARGS__", " ".join([_shell_quote(arg) for arg in ctx.attr.args])) 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}}" -workspace_root="${{runfiles_dir}}/_main" -bun_bin="${{runfiles_dir}}/_main/{bun_short_path}" -entry_point="${{runfiles_dir}}/_main/{entry_short_path}" - -resolve_entrypoint_workdir() {{ - local dir - dir="$(dirname "${{entry_point}}")" - while [[ "${{dir}}" == "${{workspace_root}}"* ]]; do - if [[ -f "${{dir}}/.env" || -f "${{dir}}/package.json" ]]; then - echo "${{dir}}" - return 0 - fi - if [[ "${{dir}}" == "${{workspace_root}}" ]]; then - break - fi - dir="$(dirname "${{dir}}")" - done - echo "$(dirname "${{entry_point}}")" -}} - -working_dir="{working_dir}" -if [[ "${{working_dir}}" == "entry_point" ]]; then - cd "$(resolve_entrypoint_workdir)" -else - cd "${{workspace_root}}" -fi - -exec "${{bun_bin}}" --bun run "${{entry_point}}" "$@" -""".format( + content = render_workspace_setup( bun_short_path = bun_bin.short_path, - entry_short_path = entry_point.short_path, - working_dir = ctx.attr.working_dir, - ), - ) - - 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, - transitive_files = depset(transitive = transitive_files), + primary_source_short_path = entry_point.short_path, + working_dir_mode = ctx.attr.working_dir, + ) + command, ) return [ + workspace_info, DefaultInfo( executable = launcher, - runfiles = runfiles, + runfiles = workspace_runfiles( + ctx, + workspace_info, + direct_files = [launcher], + transitive_files = dep_runfiles, + ), ), ] @@ -85,6 +73,9 @@ Use this rule for non-test scripts and CLIs that should run via `bazel run`. allow_files = True, doc = "Additional runtime files required by the program.", ), + "deps": attr.label_list( + doc = "Library dependencies required by the program.", + ), "working_dir": attr.string( default = "workspace", values = ["workspace", "entry_point"], diff --git a/internal/bun_bundle.bzl b/internal/bun_bundle.bzl index b41d652..781fcc4 100644 --- a/internal/bun_bundle.bzl +++ b/internal/bun_bundle.bzl @@ -1,6 +1,6 @@ """Rule for bundling JS/TS sources with Bun.""" -load("//internal:js_library.bzl", "BunSourcesInfo") +load("//internal:js_library.bzl", "collect_js_sources") def _output_name(target_name, entry): @@ -16,10 +16,7 @@ def _bun_bundle_impl(ctx): if ctx.attr.node_modules: transitive_inputs.append(ctx.attr.node_modules[DefaultInfo].files) for dep in ctx.attr.deps: - if BunSourcesInfo in dep: - transitive_inputs.append(dep[BunSourcesInfo].transitive_sources) - else: - transitive_inputs.append(dep[DefaultInfo].files) + transitive_inputs.append(collect_js_sources(dep)) outputs = [] for entry in ctx.files.entry_points: diff --git a/internal/bun_dev.bzl b/internal/bun_dev.bzl index f3bbe09..b21ed5c 100644 --- a/internal/bun_dev.bzl +++ b/internal/bun_dev.bzl @@ -1,101 +1,76 @@ """Rule for running JS/TS scripts with Bun in watch mode for development.""" +load("//internal:workspace.bzl", "create_bun_workspace_info", "render_workspace_setup", "workspace_runfiles") + def _bun_dev_impl(ctx): toolchain = ctx.toolchains["//bun:toolchain_type"] bun_bin = toolchain.bun.bun_bin entry_point = ctx.file.entry_point + workspace_info = create_bun_workspace_info( + ctx, + extra_files = ctx.files.data + ctx.files.restart_on + [bun_bin], + primary_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}}" -workspace_root="${{runfiles_dir}}/_main" -bun_bin="${{runfiles_dir}}/_main/{bun_short_path}" -entry_point="${{runfiles_dir}}/_main/{entry_short_path}" - -resolve_entrypoint_workdir() {{ - local dir - dir="$(dirname "${{entry_point}}")" - while [[ "${{dir}}" == "${{workspace_root}}"* ]]; do - if [[ -f "${{dir}}/.env" || -f "${{dir}}/package.json" ]]; then - echo "${{dir}}" - return 0 - fi - if [[ "${{dir}}" == "${{workspace_root}}" ]]; then - break - fi - dir="$(dirname "${{dir}}")" - done - echo "$(dirname "${{entry_point}}")" -}} - -working_dir="{working_dir}" -if [[ "${{working_dir}}" == "entry_point" ]]; then - cd "$(resolve_entrypoint_workdir)" -else - cd "${{workspace_root}}" -fi - -watch_mode="{watch_mode}" -if [[ "${{watch_mode}}" == "hot" ]]; then + command = """ +watch_mode="__WATCH_MODE__" +if [[ "${watch_mode}" == "hot" ]]; then dev_flag="--hot" else dev_flag="--watch" fi -run_dev() {{ - exec "${{bun_bin}}" --bun "${{dev_flag}}" run "${{entry_point}}" "$@" -}} - -if [[ {restart_count} -eq 0 ]]; then - run_dev "$@" +if [[ __RESTART_COUNT__ -eq 0 ]]; then + trap cleanup_runtime_workspace EXIT + cd "${runtime_exec_dir}" + exec "${bun_bin}" --bun "${dev_flag}" run "${primary_source}" "$@" fi readarray -t restart_paths <<'EOF_RESTART_PATHS' -{restart_watch_paths} +__RESTART_PATHS__ EOF_RESTART_PATHS -file_mtime() {{ - local p="$1" - if stat -f '%m' "${{p}}" >/dev/null 2>&1; then - stat -f '%m' "${{p}}" +file_mtime() { + local path="$1" + if stat -f '%m' "${path}" >/dev/null 2>&1; then + stat -f '%m' "${path}" return 0 fi - stat -c '%Y' "${{p}}" -}} + stat -c '%Y' "${path}" +} declare -A mtimes -for rel in "${{restart_paths[@]}}"; do - path="${{runfiles_dir}}/_main/${{rel}}" - if [[ -e "${{path}}" ]]; then - mtimes["${{rel}}"]="$(file_mtime "${{path}}")" +for rel in "${restart_paths[@]}"; do + path="${runfiles_dir}/_main/${rel}" + if [[ -e "${path}" ]]; then + mtimes["${rel}"]="$(file_mtime "${path}")" else - mtimes["${{rel}}"]="missing" + 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 +restart_child() { + if [[ -n "${child_pid}" ]] && kill -0 "${child_pid}" 2>/dev/null; then + kill "${child_pid}" + wait "${child_pid}" || true fi - "${{bun_bin}}" --bun "${{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 + ( + cd "${runtime_exec_dir}" + exec "${bun_bin}" --bun "${dev_flag}" run "${primary_source}" "$@" + ) & + child_pid=$! +} + +cleanup() { + if [[ -n "${child_pid}" ]] && kill -0 "${child_pid}" 2>/dev/null; then + kill "${child_pid}" + wait "${child_pid}" || true fi -}} + cleanup_runtime_workspace +} trap cleanup EXIT INT TERM @@ -104,45 +79,46 @@ restart_child "$@" while true; do sleep 1 changed=0 - for rel in "${{restart_paths[@]}}"; do - path="${{runfiles_dir}}/_main/${{rel}}" - if [[ -e "${{path}}" ]]; then - current="$(file_mtime "${{path}}")" + for rel in "${restart_paths[@]}"; do + path="${runfiles_dir}/_main/${rel}" + if [[ -e "${path}" ]]; then + current="$(file_mtime "${path}")" else current="missing" fi - if [[ "${{current}}" != "${{mtimes[${{rel}}]}}" ]]; then - mtimes["${{rel}}"]="${{current}}" + if [[ "${current}" != "${mtimes[${rel}]}" ]]; then + mtimes["${rel}"]="${current}" changed=1 fi done - if [[ "${{changed}}" -eq 1 ]]; then + 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, - working_dir = ctx.attr.working_dir, - restart_count = len(ctx.files.restart_on), - restart_watch_paths = restart_watch_paths, - ), +""".replace("__WATCH_MODE__", ctx.attr.watch_mode).replace( + "__RESTART_COUNT__", + str(len(ctx.files.restart_on)), + ).replace( + "__RESTART_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), + launcher = ctx.actions.declare_file(ctx.label.name) + ctx.actions.write( + output = launcher, + is_executable = True, + content = render_workspace_setup( + bun_short_path = bun_bin.short_path, + primary_source_short_path = entry_point.short_path, + working_dir_mode = ctx.attr.working_dir, + ) + command, ) return [ + workspace_info, DefaultInfo( executable = launcher, - runfiles = runfiles, + runfiles = workspace_runfiles(ctx, workspace_info, direct_files = [launcher]), ), ] diff --git a/internal/bun_install.bzl b/internal/bun_install.bzl index e84fcbb..34479be 100644 --- a/internal/bun_install.bzl +++ b/internal/bun_install.bzl @@ -6,6 +6,13 @@ _DEFAULT_INSTALL_INPUTS = [ "bunfig.toml", ] +_MANIFEST_DEP_FIELDS = [ + "dependencies", + "devDependencies", + "optionalDependencies", + "peerDependencies", +] + def _normalize_path(path): normalized = path.replace("\\", "/") if normalized.endswith("/") and normalized != "/": @@ -115,6 +122,30 @@ def _validate_catalog_shape(field, value): def _copy_json_value(value): return json.decode(json.encode(value)) +def _package_target_name(package_name): + sanitized = package_name + sanitized = sanitized.replace("@", "at_") + sanitized = sanitized.replace("/", "_") + sanitized = sanitized.replace("-", "_") + sanitized = sanitized.replace(".", "_") + sanitized = sanitized.replace("__", "_").replace("__", "_").replace("__", "_") + sanitized = sanitized.strip("_") + if not sanitized: + sanitized = "package" + return "npm__" + sanitized + +def _manifest_dependency_names(manifest): + names = {} + for field in _MANIFEST_DEP_FIELDS: + dependencies = manifest.get(field) + if dependencies == None: + continue + if type(dependencies) != type({}): + fail("bun_install: `{}` must be an object when present".format(field)) + for name in dependencies.keys(): + names[name] = True + return names + def _normalized_root_manifest(repository_ctx, package_json): manifest = json.decode(repository_ctx.read(package_json)) workspaces = manifest.get("workspaces") @@ -147,6 +178,7 @@ def _materialize_workspace_packages(repository_ctx, package_json): package_root = package_json.dirname package_root_str = str(package_root) written = {} + workspace_packages = {} for pattern in _workspace_patterns(repository_ctx, package_json): segments = pattern.split("/") @@ -168,6 +200,15 @@ def _materialize_workspace_packages(repository_ctx, package_json): repository_ctx.read(workspace_package_json), ) written[relative_dir] = True + manifest = json.decode(repository_ctx.read(workspace_package_json)) + package_name = manifest.get("name") + workspace_packages[relative_dir] = package_name if type(package_name) == type("") else "" + + package_dirs = sorted(workspace_packages.keys()) + return struct( + package_dirs = package_dirs, + package_names = [workspace_packages[package_dir] for package_dir in package_dirs if workspace_packages[package_dir]], + ) def _materialize_install_inputs(repository_ctx, package_json): package_root = package_json.dirname @@ -218,6 +259,68 @@ def _select_bun_binary(repository_ctx): fail("Unsupported host platform: os={}, arch={}".format(repository_ctx.os.name, repository_ctx.os.arch)) +def _render_package_targets_file(package_names): + lines = ["NPM_PACKAGE_TARGETS = {"] + for package_name in package_names: + lines.append(' "{}": "{}",'.format(package_name, _package_target_name(package_name))) + lines.extend([ + "}", + "", + ]) + return "\n".join(lines) + +def _render_repo_defs_bzl(repo_name): + return """load(":packages.bzl", "NPM_PACKAGE_TARGETS") + +def package_target_name(package_name): + return NPM_PACKAGE_TARGETS.get(package_name) + +def npm_link_all_packages(name = "node_modules", imported_links = []): + if not native.existing_rule(name): + native.alias( + name = name, + actual = "@{repo_name}//:node_modules", + ) + + requested = {{}} + for package_name in imported_links: + requested[package_name] = True + + for package_name, target_name in NPM_PACKAGE_TARGETS.items(): + if imported_links and package_name not in requested: + continue + if native.existing_rule(target_name): + continue + native.alias( + name = target_name, + actual = "@{repo_name}//:%s" % target_name, + ) +""".format(repo_name = repo_name) + +def _render_repo_build(package_names): + lines = [ + 'exports_files(["defs.bzl", "packages.bzl"])', + "", + "filegroup(", + ' name = "node_modules",', + ' srcs = glob(["**/node_modules/**"], allow_empty = False),', + ' visibility = ["//visibility:public"],', + ")", + "", + ] + + for package_name in package_names: + lines.extend([ + "filegroup(", + ' name = "{}",'.format(_package_target_name(package_name)), + ' srcs = glob(["node_modules/{}/**"], allow_empty = True),'.format(package_name), + ' visibility = ["//visibility:public"],', + ")", + "", + ]) + + return "\n".join(lines) + def _bun_install_repository_impl(repository_ctx): package_json = repository_ctx.path(repository_ctx.attr.package_json) bun_lockfile = repository_ctx.path(repository_ctx.attr.bun_lockfile) @@ -230,6 +333,7 @@ def _bun_install_repository_impl(repository_ctx): bun_bin = _select_bun_binary(repository_ctx) lockfile_name = bun_lockfile.basename + root_manifest = json.decode(repository_ctx.read(package_json)) if lockfile_name not in ["bun.lock", "bun.lockb"]: lockfile_name = "bun.lock" @@ -237,7 +341,7 @@ def _bun_install_repository_impl(repository_ctx): repository_ctx.file("package.json", _normalized_root_manifest(repository_ctx, package_json)) repository_ctx.symlink(bun_lockfile, lockfile_name) _materialize_install_inputs(repository_ctx, package_json) - _materialize_workspace_packages(repository_ctx, package_json) + workspace_packages = _materialize_workspace_packages(repository_ctx, package_json) install_args = [str(bun_bin), "--bun", "install", "--frozen-lockfile", "--no-progress"] if repository_ctx.attr.isolated_home: @@ -263,15 +367,26 @@ stderr: """.format(result.stdout, result.stderr)) repository_ctx.file( - "BUILD.bazel", - """filegroup( - name = "node_modules", - srcs = glob(["**/node_modules/**"], allow_empty = False), - visibility = ["//visibility:public"], -) -""", + "node_modules/.rules_bun/install.json", + json.encode({ + "bun_lockfile": lockfile_name, + "package_json": "package.json", + "workspace_package_dirs": workspace_packages.package_dirs, + }) + "\n", ) + package_names = {} + for package_name in _manifest_dependency_names(root_manifest).keys(): + package_names[package_name] = True + for package_name in workspace_packages.package_names: + package_names[package_name] = True + + sorted_package_names = sorted(package_names.keys()) + visible_repo_name = repository_ctx.attr.visible_repo_name or repository_ctx.name + repository_ctx.file("packages.bzl", _render_package_targets_file(sorted_package_names)) + repository_ctx.file("defs.bzl", _render_repo_defs_bzl(visible_repo_name)) + repository_ctx.file("BUILD.bazel", _render_repo_build(sorted_package_names)) + bun_install_repository = repository_rule( implementation = _bun_install_repository_impl, attrs = { @@ -279,6 +394,7 @@ bun_install_repository = repository_rule( "bun_lockfile": attr.label(mandatory = True, allow_single_file = True), "install_inputs": attr.label_list(allow_files = True), "isolated_home": attr.bool(default = True), + "visible_repo_name": attr.string(), "bun_linux_x64": attr.label(default = "@bun_linux_x64//:bun-linux-x64/bun", allow_single_file = True), "bun_linux_aarch64": attr.label(default = "@bun_linux_aarch64//:bun-linux-aarch64/bun", allow_single_file = True), "bun_darwin_x64": attr.label(default = "@bun_darwin_x64//:bun-darwin-x64/bun", allow_single_file = True), @@ -313,4 +429,5 @@ def bun_install(name, package_json, bun_lockfile, install_inputs = [], isolated_ bun_lockfile = bun_lockfile, install_inputs = install_inputs, isolated_home = isolated_home, + visible_repo_name = name, ) diff --git a/internal/bun_script.bzl b/internal/bun_script.bzl index 0bbc66e..b5aed91 100644 --- a/internal/bun_script.bzl +++ b/internal/bun_script.bzl @@ -1,5 +1,6 @@ """Rule for running package.json scripts with Bun.""" +load("//internal:workspace.bzl", "create_bun_workspace_info", "render_workspace_setup", "workspace_runfiles") def _shell_quote(value): return "'" + value.replace("'", "'\"'\"'") + "'" @@ -9,302 +10,37 @@ def _bun_script_impl(ctx): toolchain = ctx.toolchains["//bun:toolchain_type"] bun_bin = toolchain.bun.bun_bin package_json = ctx.file.package_json + workspace_info = create_bun_workspace_info( + ctx, + extra_files = ctx.files.data + [bun_bin], + package_dir_hint = package_json.dirname or ".", + package_json = package_json, + primary_file = package_json, + ) + command = """ +trap cleanup_runtime_workspace EXIT +cd "${runtime_exec_dir}" +exec "${bun_bin}" --bun run __SCRIPT__ "$@" +""".replace("__SCRIPT__", _shell_quote(ctx.attr.script)) 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}}" -workspace_root="${{runfiles_dir}}/_main" -workspace_root="$(cd "${{workspace_root}}" && pwd -P)" -bun_bin="${{runfiles_dir}}/_main/{bun_short_path}" -package_json="${{runfiles_dir}}/_main/{package_json_short_path}" -package_dir="$(cd "$(dirname "${{package_json}}")" && pwd -P)" -package_rel_dir="{package_rel_dir}" - -select_primary_node_modules() {{ - local selected="" - local fallback="" - while IFS= read -r node_modules_dir; do - if [[ -z "${{fallback}}" ]]; then - fallback="${{node_modules_dir}}" - fi - - if [[ ! -d "${{node_modules_dir}}/.bun" ]]; then - continue - fi - - if [[ "${{node_modules_dir}}" != *"/runfiles/_main/"* ]]; then - selected="${{node_modules_dir}}" - break - fi - - if [[ -z "${{selected}}" ]]; then - selected="${{node_modules_dir}}" - fi - done < <(find -L "${{runfiles_dir}}" -type d -name node_modules 2>/dev/null | sort) - - if [[ -n "${{selected}}" ]]; then - echo "${{selected}}" - else - echo "${{fallback}}" - fi -}} - -primary_node_modules="$(select_primary_node_modules)" - -runtime_workspace="$(mktemp -d)" -cleanup_runtime_workspace() {{ - rm -rf "${{runtime_workspace}}" -}} -trap cleanup_runtime_workspace EXIT - -runtime_package_dir="${{runtime_workspace}}/${{package_rel_dir}}" -mkdir -p "${{runtime_package_dir}}" -cp -RL "${{package_dir}}/." "${{runtime_package_dir}}/" - -workspace_package_map="${{runtime_workspace}}/workspace-packages.tsv" -python3 - "${{runtime_package_dir}}" >"${{workspace_package_map}}" <<'PY' -import json -import os -import sys - -root = sys.argv[1] - -for dirpath, dirnames, filenames in os.walk(root): - dirnames[:] = [name for name in dirnames if name != "node_modules"] - if "package.json" not in filenames: - continue - - manifest_path = os.path.join(dirpath, "package.json") - try: - with open(manifest_path, "r", encoding="utf-8") as manifest_file: - package_name = json.load(manifest_file).get("name") - except Exception: - continue - - if isinstance(package_name, str): - print(f"{{package_name}}\t{{dirpath}}") -PY - -install_repo_root="" -if [[ -n "${{primary_node_modules}}" ]]; then - install_repo_root="$(dirname "${{primary_node_modules}}")" - ln -s "${{primary_node_modules}}" "${{runtime_workspace}}/node_modules" -fi - -workspace_package_dir_for_source() {{ - local source="$1" - local manifest_path="${{source}}/package.json" - local package_name="" - local workspace_dir="" - - if [[ ! -f "${{manifest_path}}" ]]; then - return 1 - fi - - package_name="$(python3 - "${{manifest_path}}" <<'PY' -import json -import sys - -try: - with open(sys.argv[1], "r", encoding="utf-8") as manifest_file: - package_name = json.load(manifest_file).get("name", "") -except Exception: - package_name = "" - -if isinstance(package_name, str): - print(package_name) -PY -)" - - workspace_dir="$(awk -F '\t' -v name="$package_name" '$1 == name {{ print $2; exit }}' "${{workspace_package_map}}")" - if [[ -n "${{package_name}}" && -n "${{workspace_dir}}" ]]; then - echo "${{workspace_dir}}" - return 0 - fi - - return 1 -}} - -link_node_modules_entry() {{ - local source="$1" - local destination="$2" - local workspace_target="" - - rm -rf "${{destination}}" - workspace_target="$(workspace_package_dir_for_source "${{source}}" || true)" - if [[ -n "${{workspace_target}}" ]]; then - ln -s "${{workspace_target}}" "${{destination}}" - return 0 - fi - - if [[ -L "${{source}}" ]]; then - ln -s "$(readlink "${{source}}")" "${{destination}}" - else - ln -s "${{source}}" "${{destination}}" - fi -}} - -mirror_node_modules_dir() {{ - local source_dir="$1" - local destination_dir="$2" - local entry="" - local scoped_entry="" - - rm -rf "${{destination_dir}}" - mkdir -p "${{destination_dir}}" - - shopt -s dotglob nullglob - for entry in "${{source_dir}}"/* "${{source_dir}}"/.[!.]* "${{source_dir}}"/..?*; do - local entry_name="$(basename "${{entry}}")" - if [[ "${{entry_name}}" == "." || "${{entry_name}}" == ".." ]]; then - continue - fi - - if [[ -d "${{entry}}" && ! -L "${{entry}}" && "${{entry_name}}" == @* ]]; then - mkdir -p "${{destination_dir}}/${{entry_name}}" - for scoped_entry in "${{entry}}"/* "${{entry}}"/.[!.]* "${{entry}}"/..?*; do - local scoped_name="$(basename "${{scoped_entry}}")" - if [[ "${{scoped_name}}" == "." || "${{scoped_name}}" == ".." ]]; then - continue - fi - - link_node_modules_entry "${{scoped_entry}}" "${{destination_dir}}/${{entry_name}}/${{scoped_name}}" - done - continue - fi - - link_node_modules_entry "${{entry}}" "${{destination_dir}}/${{entry_name}}" - done - shopt -u dotglob nullglob -}} - -find_node_modules() {{ - local dir="$1" - local root="$2" - - while [[ "$dir" == "$root"* ]]; do - if [[ -d "$dir/node_modules" ]]; then - echo "$dir/node_modules" - return 0 - fi - - if [[ "$dir" == "$root" ]]; then - break - fi - - dir="$(dirname "$dir")" - done - - return 1 -}} - -find_install_repo_node_modules() {{ - local repo_root="$1" - local rel_dir="$2" - local candidate="${{rel_dir}}" - - while [[ -n "${{candidate}}" ]]; do - if [[ -d "${{repo_root}}/${{candidate}}/node_modules" ]]; then - echo "${{repo_root}}/${{candidate}}/node_modules" - return 0 - fi - - if [[ "${{candidate}}" != */* ]]; then - break - fi - - candidate="${{candidate#*/}}" - done - - if [[ -d "${{repo_root}}/node_modules" ]]; then - echo "${{repo_root}}/node_modules" - return 0 - fi - - return 1 -}} - -mirror_install_repo_workspace_node_modules() {{ - local repo_root="$1" - local destination_root="$2" - - while IFS= read -r install_node_modules; do - local rel_path="${{install_node_modules#${{repo_root}}/}}" - local destination="${{destination_root}}/${{rel_path}}" - - mkdir -p "$(dirname "${{destination}}")" - mirror_node_modules_dir "${{install_node_modules}}" "${{destination}}" - done < <(find "${{repo_root}}" \ - -path "${{repo_root}}/node_modules" -prune -o \ - -type d -name node_modules -print 2>/dev/null | sort) -}} - -resolved_install_node_modules="" -if [[ -n "${{install_repo_root}}" ]]; then - resolved_install_node_modules="$(find_install_repo_node_modules "${{install_repo_root}}" "${{package_rel_dir}}" || true)" -fi - -if [[ -n "${{resolved_install_node_modules}}" ]]; then - mirror_node_modules_dir "${{resolved_install_node_modules}}" "${{runtime_package_dir}}/node_modules" -else - resolved_node_modules="$(find_node_modules "${{runtime_package_dir}}" "${{runtime_workspace}}" || true)" - if [[ -n "${{resolved_node_modules}}" && "${{resolved_node_modules}}" != "${{runtime_package_dir}}/node_modules" ]]; then - mirror_node_modules_dir "${{resolved_node_modules}}" "${{runtime_package_dir}}/node_modules" - fi -fi - -if [[ -n "${{install_repo_root}}" ]]; then - mirror_install_repo_workspace_node_modules "${{install_repo_root}}" "${{runtime_package_dir}}" -fi - -path_entries=() -if [[ -d "${{runtime_package_dir}}/node_modules/.bin" ]]; then - path_entries+=("${{runtime_package_dir}}/node_modules/.bin") -fi - -if [[ -d "${{runtime_workspace}}/node_modules/.bin" && "${{runtime_workspace}}/node_modules/.bin" != "${{runtime_package_dir}}/node_modules/.bin" ]]; then - path_entries+=("${{runtime_workspace}}/node_modules/.bin") -fi - -if [[ ${{#path_entries[@]}} -gt 0 ]]; then - export PATH="$(IFS=:; echo "${{path_entries[*]}}"):${{PATH}}" -fi - -working_dir="{working_dir}" -if [[ "${{working_dir}}" == "package" ]]; then - cd "${{runtime_package_dir}}" -else - cd "${{runtime_workspace}}" -fi - -exec "${{bun_bin}}" --bun run {script} "$@" -""".format( + content = render_workspace_setup( bun_short_path = bun_bin.short_path, + package_dir_hint = package_json.dirname or ".", package_json_short_path = package_json.short_path, - package_rel_dir = package_json.dirname, - working_dir = ctx.attr.working_dir, - script = _shell_quote(ctx.attr.script), - ), - ) - - transitive_files = [] - if ctx.attr.node_modules: - transitive_files.append(ctx.attr.node_modules[DefaultInfo].files) - - runfiles = ctx.runfiles( - files = [bun_bin, package_json] + ctx.files.data, - transitive_files = depset(transitive = transitive_files), + primary_source_short_path = package_json.short_path, + working_dir_mode = ctx.attr.working_dir, + ) + command, ) return [ + workspace_info, DefaultInfo( executable = launcher, - runfiles = runfiles, + runfiles = workspace_runfiles(ctx, workspace_info, direct_files = [launcher]), ), ] diff --git a/internal/bun_test.bzl b/internal/bun_test.bzl index 75095dd..832ed3b 100644 --- a/internal/bun_test.bzl +++ b/internal/bun_test.bzl @@ -1,6 +1,7 @@ """Rule for running test suites with Bun.""" -load("//internal:js_library.bzl", "BunSourcesInfo") +load("//internal:js_library.bzl", "collect_js_runfiles") +load("//internal:workspace.bzl", "create_bun_workspace_info", "render_workspace_setup", "workspace_runfiles") def _shell_quote(value): @@ -10,53 +11,58 @@ def _shell_quote(value): def _bun_test_impl(ctx): toolchain = ctx.toolchains["//bun:toolchain_type"] bun_bin = toolchain.bun.bun_bin + primary_file = ctx.files.srcs[0] + dep_runfiles = [collect_js_runfiles(dep) for dep in ctx.attr.deps] + workspace_info = create_bun_workspace_info( + ctx, + extra_files = ctx.files.srcs + ctx.files.data + [bun_bin], + primary_file = primary_file, + ) src_args = " ".join([_shell_quote(src.short_path) for src in ctx.files.srcs]) + command = """ +trap cleanup_runtime_workspace EXIT +cd "${runtime_workspace}" +test_args=(__SRC_ARGS__) + +if [[ -n "${TESTBRIDGE_TEST_ONLY:-}" && -n "${COVERAGE_DIR:-}" ]]; then + exec "${bun_bin}" --bun test "${test_args[@]}" --test-name-pattern "${TESTBRIDGE_TEST_ONLY}" --coverage "$@" +fi +if [[ -n "${TESTBRIDGE_TEST_ONLY:-}" ]]; then + exec "${bun_bin}" --bun test "${test_args[@]}" --test-name-pattern "${TESTBRIDGE_TEST_ONLY}" "$@" +fi +if [[ -n "${COVERAGE_DIR:-}" ]]; then + exec "${bun_bin}" --bun test "${test_args[@]}" --coverage "$@" +fi +exec "${bun_bin}" --bun test "${test_args[@]}" "$@" +""".replace("__SRC_ARGS__", src_args) + if ctx.attr.args: + default_args = "\n".join(['test_args+=({})'.format(_shell_quote(arg)) for arg in ctx.attr.args]) + command = command.replace( + 'test_args=(__SRC_ARGS__)', + 'test_args=(__SRC_ARGS__)\n' + default_args, + ) + 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}" -cd "${{runfiles_dir}}/_main" - -if [[ -n "${{TESTBRIDGE_TEST_ONLY:-}}" && -n "${{COVERAGE_DIR:-}}" ]]; then - exec "${{bun_bin}}" --bun test {src_args} --test-name-pattern "${{TESTBRIDGE_TEST_ONLY}}" --coverage "$@" -fi -if [[ -n "${{TESTBRIDGE_TEST_ONLY:-}}" ]]; then - exec "${{bun_bin}}" --bun test {src_args} --test-name-pattern "${{TESTBRIDGE_TEST_ONLY}}" "$@" -fi -if [[ -n "${{COVERAGE_DIR:-}}" ]]; then - exec "${{bun_bin}}" --bun test {src_args} --coverage "$@" -fi -exec "${{bun_bin}}" --bun test {src_args} "$@" -""".format( - bun_short_path = bun_bin.short_path, - src_args = src_args, - ), + content = render_workspace_setup( + bun_short_path = bun_bin.short_path, + primary_source_short_path = primary_file.short_path, + working_dir_mode = "workspace", + ) + command, ) - - transitive_files = [] - if ctx.attr.node_modules: - transitive_files.append(ctx.attr.node_modules[DefaultInfo].files) - for dep in ctx.attr.deps: - if BunSourcesInfo in dep: - transitive_files.append(dep[BunSourcesInfo].transitive_sources) - else: - transitive_files.append(dep[DefaultInfo].files) - - runfiles = ctx.runfiles( - files = [bun_bin] + ctx.files.srcs + ctx.files.data, - transitive_files = depset(transitive = transitive_files), - ) - return [ + workspace_info, DefaultInfo( executable = launcher, - runfiles = runfiles, + runfiles = workspace_runfiles( + ctx, + workspace_info, + direct_files = [launcher], + transitive_files = dep_runfiles, + ), ), ] diff --git a/internal/js_compat.bzl b/internal/js_compat.bzl new file mode 100644 index 0000000..b3d156b --- /dev/null +++ b/internal/js_compat.bzl @@ -0,0 +1,29 @@ +"""rules_js-style compatibility exports backed by Bun.""" + +load("//internal:bun_binary.bzl", _bun_binary = "bun_binary") +load("//internal:bun_test.bzl", _bun_test = "bun_test") +load("//internal:js_library.bzl", _JsInfo = "JsInfo", _js_library = "js_library", _ts_library = "ts_library") +load("//internal:js_run_devserver.bzl", _js_run_devserver = "js_run_devserver") + +JsInfo = _JsInfo +js_library = _js_library +ts_library = _ts_library +js_run_devserver = _js_run_devserver + +def js_binary(name, **kwargs): + _bun_binary(name = name, **kwargs) + +def js_test(name, entry_point = None, srcs = None, **kwargs): + if entry_point != None: + if srcs != None: + fail("js_test accepts either `entry_point` or `srcs`, but not both") + srcs = [entry_point] + + if srcs == None: + fail("js_test requires `entry_point` or `srcs`") + + _bun_test( + name = name, + srcs = srcs, + **kwargs + ) diff --git a/internal/js_library.bzl b/internal/js_library.bzl index b99b8d4..87a0c78 100644 --- a/internal/js_library.bzl +++ b/internal/js_library.bzl @@ -1,23 +1,74 @@ """Lightweight JS/TS source grouping rules.""" +JsInfo = provider( + doc = "Provides transitive JavaScript/TypeScript metadata for Bun and JS compatibility rules.", + fields = { + "sources": "Direct source files owned by this target.", + "transitive_sources": "Transitive source files from this target and its deps.", + "types": "Direct type files owned by this target.", + "transitive_types": "Transitive type files from this target and its deps.", + "data_files": "Direct runtime data files owned by this target.", + "transitive_runfiles": "Transitive runtime files from this target and its deps.", + }, +) + BunSourcesInfo = provider( "Provides transitive sources for Bun libraries.", fields = ["transitive_sources"], ) +def collect_js_sources(dep): + if JsInfo in dep: + return dep[JsInfo].transitive_sources + if BunSourcesInfo in dep: + return dep[BunSourcesInfo].transitive_sources + return dep[DefaultInfo].files + +def collect_js_runfiles(dep): + if JsInfo in dep: + return dep[JsInfo].transitive_runfiles + if BunSourcesInfo in dep: + return dep[BunSourcesInfo].transitive_sources + return dep[DefaultInfo].files + def _bun_library_impl(ctx): - transitive_sources = [ - dep[BunSourcesInfo].transitive_sources + transitive_sources = [collect_js_sources(dep) for dep in ctx.attr.deps] + transitive_types = [ + dep[JsInfo].transitive_types for dep in ctx.attr.deps - if BunSourcesInfo in dep + if JsInfo in dep ] + transitive_runfiles = [collect_js_runfiles(dep) for dep in ctx.attr.deps] + all_sources = depset( direct = ctx.files.srcs, transitive = transitive_sources, ) + all_types = depset( + direct = ctx.files.types, + transitive = transitive_types, + ) + all_runfiles = depset( + direct = ctx.files.srcs + ctx.files.types + ctx.files.data, + transitive = transitive_runfiles, + ) + default_files = depset( + direct = ctx.files.srcs + ctx.files.types + ctx.files.data, + transitive = transitive_sources + transitive_types + transitive_runfiles, + ) + + js_info = JsInfo( + sources = depset(ctx.files.srcs), + transitive_sources = all_sources, + types = depset(ctx.files.types), + transitive_types = all_types, + data_files = depset(ctx.files.data), + transitive_runfiles = all_runfiles, + ) return [ + js_info, BunSourcesInfo(transitive_sources = all_sources), - DefaultInfo(files = all_sources), + DefaultInfo(files = default_files), ] js_library = rule( @@ -28,6 +79,14 @@ js_library = rule( allow_files = [".js", ".jsx", ".mjs", ".cjs"], doc = "JavaScript source files in this library.", ), + "types": attr.label_list( + allow_files = [".d.ts"], + doc = "Optional declaration files associated with this library.", + ), + "data": attr.label_list( + allow_files = True, + doc = "Optional runtime files propagated to dependents.", + ), "deps": attr.label_list( doc = "Other Bun source libraries to include transitively.", ), @@ -42,6 +101,14 @@ ts_library = rule( allow_files = [".ts", ".tsx"], doc = "TypeScript source files in this library.", ), + "types": attr.label_list( + allow_files = [".d.ts"], + doc = "Optional declaration files associated with this library.", + ), + "data": attr.label_list( + allow_files = True, + doc = "Optional runtime files propagated to dependents.", + ), "deps": attr.label_list( doc = "Other Bun source libraries to include transitively.", ), diff --git a/internal/js_run_devserver.bzl b/internal/js_run_devserver.bzl new file mode 100644 index 0000000..96211d9 --- /dev/null +++ b/internal/js_run_devserver.bzl @@ -0,0 +1,100 @@ +"""Compatibility rule for running an executable target as a dev server.""" + +load("//internal:js_library.bzl", "collect_js_runfiles") +load("//internal:workspace.bzl", "create_bun_workspace_info", "render_workspace_setup", "workspace_runfiles") + +def _shell_quote(value): + return "'" + value.replace("'", "'\"'\"'") + "'" + +def _js_run_devserver_impl(ctx): + toolchain = ctx.toolchains["//bun:toolchain_type"] + bun_bin = toolchain.bun.bun_bin + package_json = ctx.file.package_json + dep_runfiles = [collect_js_runfiles(dep) for dep in ctx.attr.deps] + tool_default_info = ctx.attr.tool[DefaultInfo] + + workspace_info = create_bun_workspace_info( + ctx, + primary_file = package_json or tool_default_info.files_to_run.executable, + package_json = package_json, + package_dir_hint = ctx.attr.package_dir_hint, + extra_files = ctx.files.data + [bun_bin, tool_default_info.files_to_run.executable], + ) + + tool_workspace = ctx.attr.tool.label.workspace_name or "_main" + tool_path = "{}/{}".format(tool_workspace, tool_default_info.files_to_run.executable.short_path) + default_args = " ".join([_shell_quote(arg) for arg in ctx.attr.args]) + + launcher = ctx.actions.declare_file(ctx.label.name) + ctx.actions.write( + output = launcher, + is_executable = True, + content = render_workspace_setup( + bun_short_path = bun_bin.short_path, + primary_source_short_path = package_json.short_path if package_json else tool_default_info.files_to_run.executable.short_path, + package_json_short_path = package_json.short_path if package_json else "", + package_dir_hint = ctx.attr.package_dir_hint, + working_dir_mode = ctx.attr.working_dir, + ) + """ +trap cleanup_runtime_workspace EXIT +cd "${runtime_exec_dir}" +tool="${runfiles_dir}/__TOOL_SHORT_PATH__" +exec "${tool}" __DEFAULT_ARGS__ "$@" +""".replace("__TOOL_SHORT_PATH__", tool_path).replace("__DEFAULT_ARGS__", default_args), + ) + + return [ + workspace_info, + DefaultInfo( + executable = launcher, + runfiles = workspace_runfiles( + ctx, + workspace_info, + direct_files = [launcher, tool_default_info.files_to_run.executable], + transitive_files = dep_runfiles, + ).merge(tool_default_info.default_runfiles), + ), + ] + +js_run_devserver = rule( + implementation = _js_run_devserver_impl, + doc = """Runs an executable target from a staged JS workspace. + +This is a Bun-backed compatibility adapter for `rules_js`-style devserver +targets. It stages the same runtime workspace as the Bun rules, then executes +the provided tool with any default arguments. +""", + attrs = { + "tool": attr.label( + mandatory = True, + executable = True, + cfg = "target", + doc = "Executable target to launch as the dev server.", + ), + "package_json": attr.label( + allow_single_file = True, + doc = "Optional package.json used to resolve the package working directory.", + ), + "package_dir_hint": attr.string( + default = ".", + doc = "Optional package-relative directory hint when package_json is not supplied.", + ), + "node_modules": attr.label( + doc = "Optional label providing package files from a node_modules tree, typically produced by bun_install or npm_translate_lock, in runfiles.", + ), + "deps": attr.label_list( + doc = "Library dependencies required by the dev server.", + ), + "data": attr.label_list( + allow_files = True, + doc = "Additional runtime files required by the dev server.", + ), + "working_dir": attr.string( + default = "workspace", + values = ["workspace", "package"], + doc = "Working directory at runtime: Bazel runfiles workspace root or the resolved package directory.", + ), + }, + executable = True, + toolchains = ["//bun:toolchain_type"], +) diff --git a/internal/workspace.bzl b/internal/workspace.bzl new file mode 100644 index 0000000..a7c9691 --- /dev/null +++ b/internal/workspace.bzl @@ -0,0 +1,649 @@ +"""Shared Bun workspace metadata and launcher helpers.""" + +BunWorkspaceInfo = provider( + doc = "Workspace/runtime metadata shared by Bun rules and adapters.", + fields = { + "install_metadata_file": "Optional install metadata file from bun_install.", + "metadata_file": "Rule-local metadata file describing the staged workspace inputs.", + "node_modules_files": "Depset of node_modules files from bun_install.", + "package_dir_hint": "Package-relative directory when known at analysis time.", + "package_json": "Package manifest file when explicitly provided.", + "primary_file": "Primary source file used to resolve the runtime package context.", + "runtime_files": "Depset of runtime files required to stage the workspace.", + }, +) + +_WORKSPACE_SETUP_TEMPLATE = """#!/usr/bin/env bash +set -euo pipefail + +runfiles_dir="${RUNFILES_DIR:-$0.runfiles}" +workspace_root="${runfiles_dir}/_main" +workspace_root="$(cd "${workspace_root}" && pwd -P)" +bun_bin="${runfiles_dir}/_main/__BUN_SHORT_PATH__" +primary_source="" +if [[ -n "__PRIMARY_SOURCE_SHORT_PATH__" ]]; then + primary_source="${runfiles_dir}/_main/__PRIMARY_SOURCE_SHORT_PATH__" +fi +package_json="" +if [[ -n "__PACKAGE_JSON_SHORT_PATH__" ]]; then + package_json="${runfiles_dir}/_main/__PACKAGE_JSON_SHORT_PATH__" +fi +package_rel_dir_hint="__PACKAGE_DIR_HINT__" +working_dir_mode="__WORKING_DIR_MODE__" + +normalize_rel_dir() { + local value="$1" + if [[ -z "${value}" || "${value}" == "." ]]; then + echo "." + else + echo "${value#./}" + fi +} + +dirname_rel_dir() { + local value + value="$(normalize_rel_dir "$1")" + if [[ "${value}" == "." || "${value}" != */* ]]; then + echo "." + return 0 + fi + echo "${value%/*}" +} + +first_path_component() { + local value + value="$(normalize_rel_dir "$1")" + if [[ "${value}" == "." ]]; then + echo "" + return 0 + fi + echo "${value%%/*}" +} + +rel_dir_from_abs_path() { + local absolute_path="$1" + if [[ "${absolute_path}" == "${workspace_root}" ]]; then + echo "." + return 0 + fi + echo "${absolute_path#"${workspace_root}/"}" +} + +find_package_rel_dir_for_path() { + local path="$1" + local dir="$1" + if [[ -f "${dir}" ]]; then + dir="$(dirname "${dir}")" + fi + + while [[ "${dir}" == "${workspace_root}"* ]]; do + if [[ -f "${dir}/package.json" ]]; then + rel_dir_from_abs_path "${dir}" + return 0 + fi + if [[ "${dir}" == "${workspace_root}" ]]; then + break + fi + dir="$(dirname "${dir}")" + done + + rel_dir_from_abs_path "$(dirname "${path}")" +} + +find_working_rel_dir_for_path() { + local path="$1" + local dir="$1" + if [[ -f "${dir}" ]]; then + dir="$(dirname "${dir}")" + fi + + while [[ "${dir}" == "${workspace_root}"* ]]; do + if [[ -f "${dir}/.env" || -f "${dir}/package.json" ]]; then + rel_dir_from_abs_path "${dir}" + return 0 + fi + if [[ "${dir}" == "${workspace_root}" ]]; then + break + fi + dir="$(dirname "${dir}")" + done + + rel_dir_from_abs_path "$(dirname "${path}")" +} + +select_primary_node_modules() { + local selected="" + local fallback="" + while IFS= read -r node_modules_dir; do + if [[ -z "${fallback}" ]]; then + fallback="${node_modules_dir}" + fi + + if [[ ! -d "${node_modules_dir}/.bun" ]]; then + continue + fi + + if [[ "${node_modules_dir}" != *"/runfiles/_main/"* ]]; then + selected="${node_modules_dir}" + break + fi + + if [[ -z "${selected}" ]]; then + selected="${node_modules_dir}" + fi + done < <(find -L "${runfiles_dir}" -type d -name node_modules 2>/dev/null | sort) + + if [[ -n "${selected}" ]]; then + echo "${selected}" + else + echo "${fallback}" + fi +} + +link_top_level_entries() { + local source_root="$1" + local destination_root="$2" + local skipped_entry="$3" + local entry="" + local entry_name="" + + shopt -s dotglob nullglob + for entry in "${source_root}"/* "${source_root}"/.[!.]* "${source_root}"/..?*; do + entry_name="$(basename "${entry}")" + if [[ "${entry_name}" == "." || "${entry_name}" == ".." ]]; then + continue + fi + if [[ -n "${skipped_entry}" && "${entry_name}" == "${skipped_entry}" ]]; then + continue + fi + ln -s "${entry}" "${destination_root}/${entry_name}" + done + shopt -u dotglob nullglob +} + +materialize_package_path() { + local source_root="$1" + local destination_root="$2" + local package_rel_dir + package_rel_dir="$(normalize_rel_dir "$3")" + + if [[ "${package_rel_dir}" == "." ]]; then + return 0 + fi + + local source_cursor="${source_root}" + local destination_cursor="${destination_root}" + local parts=() + local current="${package_rel_dir}" + + while [[ -n "${current}" ]]; do + if [[ "${current}" == */* ]]; then + parts+=("${current%%/*}") + current="${current#*/}" + else + parts+=("${current}") + break + fi + done + + local index=0 + while [[ ${index} -lt $((${#parts[@]} - 1)) ]]; do + local part="${parts[${index}]}" + local next_part="${parts[$((index + 1))]}" + source_cursor="${source_cursor}/${part}" + destination_cursor="${destination_cursor}/${part}" + mkdir -p "${destination_cursor}" + + local sibling="" + local sibling_name="" + shopt -s dotglob nullglob + for sibling in "${source_cursor}"/* "${source_cursor}"/.[!.]* "${source_cursor}"/..?*; do + sibling_name="$(basename "${sibling}")" + if [[ "${sibling_name}" == "." || "${sibling_name}" == ".." || "${sibling_name}" == "${next_part}" ]]; then + continue + fi + if [[ ! -e "${destination_cursor}/${sibling_name}" ]]; then + ln -s "${sibling}" "${destination_cursor}/${sibling_name}" + fi + done + shopt -u dotglob nullglob + index=$((index + 1)) + done + + mkdir -p "${destination_root}/${package_rel_dir}" +} + +materialize_directory_entries() { + local source_root="$1" + local destination_root="$2" + local entry="" + local entry_name="" + + mkdir -p "${destination_root}" + shopt -s dotglob nullglob + for entry in "${source_root}"/* "${source_root}"/.[!.]* "${source_root}"/..?*; do + entry_name="$(basename "${entry}")" + if [[ "${entry_name}" == "." || "${entry_name}" == ".." ]]; then + continue + fi + rm -rf "${destination_root}/${entry_name}" + ln -s "${entry}" "${destination_root}/${entry_name}" + done + shopt -u dotglob nullglob +} + +stage_workspace_view() { + local source_root="$1" + local destination_root="$2" + local package_rel_dir + package_rel_dir="$(normalize_rel_dir "$3")" + local skipped_entry + skipped_entry="$(first_path_component "${package_rel_dir}")" + + link_top_level_entries "${source_root}" "${destination_root}" "${skipped_entry}" + + if [[ "${package_rel_dir}" == "." ]]; then + return 0 + fi + + materialize_package_path "${source_root}" "${destination_root}" "${package_rel_dir}" + materialize_directory_entries "${source_root}/${package_rel_dir}" "${destination_root}/${package_rel_dir}" +} + +build_workspace_package_map() { + local root="$1" + local out="$2" + + python3 - "${root}" >"${out}" <<'PY' +import json +import os +import sys + +root = os.path.abspath(sys.argv[1]) + +for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = [name for name in dirnames if name != "node_modules"] + if "package.json" not in filenames: + continue + + manifest_path = os.path.join(dirpath, "package.json") + try: + with open(manifest_path, "r", encoding="utf-8") as manifest_file: + package_name = json.load(manifest_file).get("name") + except Exception: + continue + + if not isinstance(package_name, str): + continue + + rel_dir = os.path.relpath(dirpath, root) + if rel_dir == ".": + rel_dir = "." + print(f"{package_name}\t{rel_dir}") +PY +} + +workspace_package_rel_dir_for_source() { + local source="$1" + local manifest_path="${source}/package.json" + local package_name="" + + if [[ ! -f "${manifest_path}" ]]; then + return 1 + fi + + package_name="$(python3 - "${manifest_path}" <<'PY' +import json +import sys + +try: + with open(sys.argv[1], "r", encoding="utf-8") as manifest_file: + package_name = json.load(manifest_file).get("name", "") +except Exception: + package_name = "" + +if isinstance(package_name, str): + print(package_name) +PY +)" + + if [[ -z "${package_name}" ]]; then + return 1 + fi + + awk -F '\t' -v name="${package_name}" '$1 == name { print $2; exit }' "${workspace_package_map}" +} + +link_node_modules_entry() { + local source="$1" + local destination="$2" + local workspace_rel_dir="" + + rm -rf "${destination}" + workspace_rel_dir="$(workspace_package_rel_dir_for_source "${source}" || true)" + if [[ -n "${workspace_rel_dir}" ]]; then + ln -s "${runtime_workspace}/${workspace_rel_dir}" "${destination}" + return 0 + fi + + if [[ -L "${source}" ]]; then + ln -s "$(readlink "${source}")" "${destination}" + else + ln -s "${source}" "${destination}" + fi +} + +mirror_node_modules_dir() { + local source_dir="$1" + local destination_dir="$2" + local entry="" + local entry_name="" + local scoped_entry="" + local scoped_name="" + + rm -rf "${destination_dir}" + mkdir -p "${destination_dir}" + + shopt -s dotglob nullglob + for entry in "${source_dir}"/* "${source_dir}"/.[!.]* "${source_dir}"/..?*; do + entry_name="$(basename "${entry}")" + if [[ "${entry_name}" == "." || "${entry_name}" == ".." || "${entry_name}" == ".rules_bun" ]]; then + continue + fi + + if [[ -d "${entry}" && ! -L "${entry}" && "${entry_name}" == @* ]]; then + mkdir -p "${destination_dir}/${entry_name}" + for scoped_entry in "${entry}"/* "${entry}"/.[!.]* "${entry}"/..?*; do + scoped_name="$(basename "${scoped_entry}")" + if [[ "${scoped_name}" == "." || "${scoped_name}" == ".." ]]; then + continue + fi + link_node_modules_entry "${scoped_entry}" "${destination_dir}/${entry_name}/${scoped_name}" + done + continue + fi + + link_node_modules_entry "${entry}" "${destination_dir}/${entry_name}" + done + shopt -u dotglob nullglob +} + +find_install_repo_node_modules() { + local repo_root="$1" + local package_rel_dir + package_rel_dir="$(normalize_rel_dir "$2")" + + if [[ "${package_rel_dir}" != "." ]]; then + local candidate="${package_rel_dir}" + while true; do + if [[ -d "${repo_root}/${candidate}/node_modules" ]]; then + echo "${repo_root}/${candidate}/node_modules" + return 0 + fi + + if [[ "${candidate}" != */* ]]; then + break + fi + candidate="${candidate%/*}" + done + fi + + if [[ -d "${repo_root}/node_modules" ]]; then + echo "${repo_root}/node_modules" + return 0 + fi + + return 1 +} + +mirror_install_repo_workspace_node_modules() { + local repo_root="$1" + local destination_root="$2" + + while IFS= read -r install_node_modules; do + local rel_path="${install_node_modules#${repo_root}/}" + local destination="${destination_root}/${rel_path}" + + mkdir -p "$(dirname "${destination}")" + mirror_node_modules_dir "${install_node_modules}" "${destination}" + done < <(find "${repo_root}" \ + -path "${repo_root}/node_modules" -prune -o \ + -type d -name node_modules -print 2>/dev/null | sort) +} + +build_runtime_path() { + local workspace_dir="$1" + local package_dir="$2" + local entries=() + + if [[ -d "${package_dir}/node_modules/.bin" ]]; then + entries+=("${package_dir}/node_modules/.bin") + fi + if [[ -d "${workspace_dir}/node_modules/.bin" && "${workspace_dir}/node_modules/.bin" != "${package_dir}/node_modules/.bin" ]]; then + entries+=("${workspace_dir}/node_modules/.bin") + fi + if [[ -n "${PATH:-}" ]]; then + entries+=("${PATH}") + fi + + if [[ ${#entries[@]} -eq 0 ]]; then + echo "" + return 0 + fi + + local path_value="" + local entry="" + for entry in "${entries[@]}"; do + if [[ -z "${path_value}" ]]; then + path_value="${entry}" + else + path_value="${path_value}:${entry}" + fi + done + echo "${path_value}" +} + +resolve_package_rel_dir() { + if [[ -n "${package_rel_dir_hint}" && "${package_rel_dir_hint}" != "." ]]; then + normalize_rel_dir "${package_rel_dir_hint}" + return 0 + fi + if [[ -n "${package_json}" ]]; then + find_package_rel_dir_for_path "${package_json}" + return 0 + fi + if [[ -n "${primary_source}" ]]; then + find_package_rel_dir_for_path "${primary_source}" + return 0 + fi + echo "." +} + +resolve_execution_rel_dir() { + local package_rel_dir="$1" + case "${working_dir_mode}" in + workspace) + echo "." + ;; + package) + echo "${package_rel_dir}" + ;; + entry_point) + if [[ -n "${primary_source}" ]]; then + find_working_rel_dir_for_path "${primary_source}" + else + echo "${package_rel_dir}" + fi + ;; + *) + echo "${package_rel_dir}" + ;; + esac +} + +package_rel_dir="$(resolve_package_rel_dir)" +execution_rel_dir="$(resolve_execution_rel_dir "${package_rel_dir}")" + +runtime_workspace="$(mktemp -d)" +cleanup_runtime_workspace() { + rm -rf "${runtime_workspace}" +} + +stage_workspace_view "${workspace_root}" "${runtime_workspace}" "${package_rel_dir}" +runtime_package_dir="${runtime_workspace}" +if [[ "${package_rel_dir}" != "." ]]; then + runtime_package_dir="${runtime_workspace}/${package_rel_dir}" +fi +runtime_exec_dir="${runtime_workspace}" +if [[ "${execution_rel_dir}" != "." ]]; then + runtime_exec_dir="${runtime_workspace}/${execution_rel_dir}" +fi + +workspace_package_map="${runtime_workspace}/.rules_bun_workspace_packages.tsv" +build_workspace_package_map "${runtime_workspace}" "${workspace_package_map}" + +primary_node_modules="$(select_primary_node_modules)" +install_repo_root="" +if [[ -n "${primary_node_modules}" ]]; then + install_repo_root="$(dirname "${primary_node_modules}")" + mirror_node_modules_dir "${primary_node_modules}" "${runtime_workspace}/node_modules" +fi + +if [[ -n "${install_repo_root}" ]]; then + resolved_install_node_modules="$(find_install_repo_node_modules "${install_repo_root}" "${package_rel_dir}" || true)" + if [[ -n "${resolved_install_node_modules}" && "${resolved_install_node_modules}" != "${install_repo_root}/node_modules" ]]; then + mirror_node_modules_dir "${resolved_install_node_modules}" "${runtime_package_dir}/node_modules" + fi + mirror_install_repo_workspace_node_modules "${install_repo_root}" "${runtime_workspace}" +fi + +if [[ ! -e "${runtime_package_dir}/node_modules" && -e "${runtime_workspace}/node_modules" && "${runtime_package_dir}" != "${runtime_workspace}" ]]; then + ln -s "${runtime_workspace}/node_modules" "${runtime_package_dir}/node_modules" +fi + +runtime_path="$(build_runtime_path "${runtime_workspace}" "${runtime_package_dir}")" +if [[ -n "${runtime_path}" ]]; then + export PATH="${runtime_path}" +fi +""" + +def _shell_quote(value): + return "'" + value.replace("'", "'\"'\"'") + "'" + +def _dirname(path): + if not path or path == ".": + return "." + + index = path.rfind("/") + if index < 0: + return "." + if index == 0: + return "/" + return path[:index] + +def find_install_metadata_file(files): + for file in files: + if file.short_path.endswith("node_modules/.rules_bun/install.json"): + return file + return None + +def resolve_node_modules_roots(files, workspace_dir = ""): + install_metadata_file = find_install_metadata_file(files) + shared_node_modules_root = None + workspace_node_modules_root = None + + if install_metadata_file: + shared_node_modules_root = _dirname(_dirname(install_metadata_file.path)) + + workspace_marker = "" + if workspace_dir: + workspace_marker = "/%s/node_modules/" % workspace_dir.strip("/") + + shortest_path = None + for src in files: + if workspace_marker and workspace_marker in src.path and workspace_node_modules_root == None: + workspace_node_modules_root = src.path[:src.path.find(workspace_marker) + len(workspace_marker) - 1] + if shortest_path == None or len(src.path) < len(shortest_path): + shortest_path = src.path + + if shared_node_modules_root == None and shortest_path: + marker = "/node_modules/" + marker_index = shortest_path.find(marker) + if marker_index >= 0: + shared_node_modules_root = shortest_path[:marker_index + len("/node_modules")] + + return struct( + install_metadata_file = install_metadata_file, + node_modules_root = workspace_node_modules_root or shared_node_modules_root, + shared_node_modules_root = shared_node_modules_root, + ) + +def create_bun_workspace_info(ctx, primary_file = None, package_json = None, package_dir_hint = ".", extra_files = None): + direct_runtime_files = [] + if primary_file: + direct_runtime_files.append(primary_file) + if package_json and package_json != primary_file: + direct_runtime_files.append(package_json) + direct_runtime_files.extend(extra_files or []) + + node_modules_files = depset() + install_metadata_file = None + if getattr(ctx.attr, "node_modules", None): + node_modules_files = ctx.attr.node_modules[DefaultInfo].files + install_metadata_file = find_install_metadata_file(node_modules_files.to_list()) + + metadata_file = ctx.actions.declare_file(ctx.label.name + ".bun_workspace.json") + ctx.actions.write( + output = metadata_file, + content = json.encode({ + "install_metadata": install_metadata_file.short_path if install_metadata_file else "", + "package_dir_hint": package_dir_hint or ".", + "package_json": package_json.short_path if package_json else "", + "primary_file": primary_file.short_path if primary_file else "", + }) + "\n", + ) + direct_runtime_files.append(metadata_file) + + runtime_files = depset( + direct = direct_runtime_files, + transitive = [node_modules_files], + ) + + return BunWorkspaceInfo( + install_metadata_file = install_metadata_file, + metadata_file = metadata_file, + node_modules_files = node_modules_files, + package_dir_hint = package_dir_hint or ".", + package_json = package_json, + primary_file = primary_file, + runtime_files = runtime_files, + ) + +def workspace_runfiles(ctx, workspace_info, direct_files = None, transitive_files = None): + return ctx.runfiles( + files = direct_files or [], + transitive_files = depset( + transitive = [workspace_info.runtime_files] + (transitive_files or []), + ), + ) + +def render_workspace_setup( + bun_short_path, + working_dir_mode, + primary_source_short_path = "", + package_json_short_path = "", + package_dir_hint = "."): + return _WORKSPACE_SETUP_TEMPLATE.replace("__BUN_SHORT_PATH__", bun_short_path).replace( + "__PRIMARY_SOURCE_SHORT_PATH__", + primary_source_short_path, + ).replace( + "__PACKAGE_JSON_SHORT_PATH__", + package_json_short_path, + ).replace( + "__PACKAGE_DIR_HINT__", + package_dir_hint or ".", + ).replace( + "__WORKING_DIR_MODE__", + working_dir_mode, + ) + diff --git a/js/BUILD.bazel b/js/BUILD.bazel new file mode 100644 index 0000000..fb0947c --- /dev/null +++ b/js/BUILD.bazel @@ -0,0 +1,12 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package(default_visibility = ["//visibility:public"]) + +exports_files(["defs.bzl"]) + +bzl_library( + name = "defs_bzl", + srcs = ["defs.bzl"], + visibility = ["//visibility:public"], + deps = ["//internal:js_compat_bzl"], +) diff --git a/js/defs.bzl b/js/defs.bzl new file mode 100644 index 0000000..552cab9 --- /dev/null +++ b/js/defs.bzl @@ -0,0 +1,12 @@ +"""rules_js-style public API backed by Bun.""" + +load("//internal:js_compat.bzl", _JsInfo = "JsInfo", _js_binary = "js_binary", _js_library = "js_library", _js_run_devserver = "js_run_devserver", _js_test = "js_test", _ts_library = "ts_library") + +visibility("public") + +JsInfo = _JsInfo +js_binary = _js_binary +js_test = _js_test +js_run_devserver = _js_run_devserver +js_library = _js_library +ts_library = _ts_library diff --git a/npm/BUILD.bazel b/npm/BUILD.bazel new file mode 100644 index 0000000..7fbb71a --- /dev/null +++ b/npm/BUILD.bazel @@ -0,0 +1,22 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "extensions.bzl", + "repositories.bzl", +]) + +bzl_library( + name = "extensions_bzl", + srcs = ["extensions.bzl"], + visibility = ["//visibility:public"], + deps = ["//internal:bun_install_bzl"], +) + +bzl_library( + name = "repositories_bzl", + srcs = ["repositories.bzl"], + visibility = ["//visibility:public"], + deps = ["//internal:bun_install_bzl"], +) diff --git a/npm/extensions.bzl b/npm/extensions.bzl new file mode 100644 index 0000000..a3779d6 --- /dev/null +++ b/npm/extensions.bzl @@ -0,0 +1,28 @@ +load("//internal:bun_install.bzl", "bun_install_repository") + +_translate = tag_class( + attrs = { + "name": attr.string(mandatory = True), + "package_json": attr.label(mandatory = True), + "lockfile": attr.label(mandatory = True), + "install_inputs": attr.label_list(allow_files = True), + "isolated_home": attr.bool(default = True), + }, +) + +def _npm_translate_lock_impl(ctx): + for mod in ctx.modules: + for install in mod.tags.translate: + bun_install_repository( + name = install.name, + package_json = install.package_json, + bun_lockfile = install.lockfile, + install_inputs = install.install_inputs, + isolated_home = install.isolated_home, + visible_repo_name = install.name, + ) + +npm_translate_lock = module_extension( + implementation = _npm_translate_lock_impl, + tag_classes = {"translate": _translate}, +) diff --git a/npm/repositories.bzl b/npm/repositories.bzl new file mode 100644 index 0000000..fed21a5 --- /dev/null +++ b/npm/repositories.bzl @@ -0,0 +1,11 @@ +load("//internal:bun_install.bzl", "bun_install_repository") + +def npm_translate_lock(name, package_json, lockfile, install_inputs = [], isolated_home = True): + bun_install_repository( + name = name, + package_json = package_json, + bun_lockfile = lockfile, + install_inputs = install_inputs, + isolated_home = isolated_home, + visible_repo_name = name, + ) diff --git a/tests/install_extension_test/BUILD.bazel b/tests/install_extension_test/BUILD.bazel index bdf75d8..9b1e30d 100644 --- a/tests/install_extension_test/BUILD.bazel +++ b/tests/install_extension_test/BUILD.bazel @@ -6,3 +6,10 @@ sh_test( args = ["$(location //bun:extensions.bzl)"], data = ["//bun:extensions.bzl"], ) + +sh_test( + name = "npm_translate_lock_extension_shape_test", + srcs = ["npm_extension_shape_test.sh"], + args = ["$(location //npm:extensions.bzl)"], + data = ["//npm:extensions.bzl"], +) diff --git a/tests/install_extension_test/npm_extension_shape_test.sh b/tests/install_extension_test/npm_extension_shape_test.sh new file mode 100755 index 0000000..3036148 --- /dev/null +++ b/tests/install_extension_test/npm_extension_shape_test.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +extension_file="$1" + +grep -Eq 'npm_translate_lock[[:space:]]*=[[:space:]]*module_extension\(' "${extension_file}" +grep -Eq 'tag_classes[[:space:]]*=[[:space:]]*\{"translate":[[:space:]]*_translate\}' "${extension_file}" +grep -Eq '"name":[[:space:]]*attr\.string\(mandatory[[:space:]]*=[[:space:]]*True\)' "${extension_file}" +grep -Eq '"package_json":[[:space:]]*attr\.label\(mandatory[[:space:]]*=[[:space:]]*True\)' "${extension_file}" +grep -Eq '"lockfile":[[:space:]]*attr\.label\(mandatory[[:space:]]*=[[:space:]]*True\)' "${extension_file}" diff --git a/tests/js_compat_test/BUILD.bazel b/tests/js_compat_test/BUILD.bazel new file mode 100644 index 0000000..5363bf7 --- /dev/null +++ b/tests/js_compat_test/BUILD.bazel @@ -0,0 +1,41 @@ +load("//js:defs.bzl", "js_binary", "js_run_devserver", "js_test", "ts_library") +load("@rules_shell//shell:sh_test.bzl", "sh_test") + +ts_library( + name = "helper_lib", + srcs = ["helper.ts"], + data = ["payload.txt"], +) + +js_binary( + name = "compat_bin", + entry_point = "main.ts", + deps = [":helper_lib"], + args = ["compat-mode"], +) + +sh_test( + name = "js_binary_compat_test", + srcs = ["run_binary.sh"], + args = ["$(location :compat_bin)"], + data = [":compat_bin"], +) + +js_test( + name = "compat_suite", + entry_point = "app.test.ts", + deps = [":helper_lib"], +) + +js_run_devserver( + name = "compat_devserver", + tool = ":compat_bin", + args = ["devserver-mode"], +) + +sh_test( + name = "js_run_devserver_compat_test", + srcs = ["run_devserver.sh"], + args = ["$(location :compat_devserver)"], + data = [":compat_devserver"], +) diff --git a/tests/js_compat_test/app.test.ts b/tests/js_compat_test/app.test.ts new file mode 100644 index 0000000..7a8de3c --- /dev/null +++ b/tests/js_compat_test/app.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from "bun:test"; + +import { helperMessage } from "./helper.ts"; + +test("js_test compatibility layer propagates deps and data", () => { + expect(helperMessage()).toBe("helper:payload-from-lib"); +}); diff --git a/tests/js_compat_test/helper.ts b/tests/js_compat_test/helper.ts new file mode 100644 index 0000000..12b839d --- /dev/null +++ b/tests/js_compat_test/helper.ts @@ -0,0 +1,6 @@ +import { readFileSync } from "node:fs"; + +export function helperMessage(): string { + const payload = readFileSync(new URL("./payload.txt", import.meta.url), "utf8").trim(); + return `helper:${payload}`; +} diff --git a/tests/js_compat_test/main.ts b/tests/js_compat_test/main.ts new file mode 100644 index 0000000..699ff5d --- /dev/null +++ b/tests/js_compat_test/main.ts @@ -0,0 +1,3 @@ +import { helperMessage } from "./helper.ts"; + +console.log(`${helperMessage()} ${Bun.argv.slice(2).join(" ")}`.trim()); diff --git a/tests/js_compat_test/payload.txt b/tests/js_compat_test/payload.txt new file mode 100644 index 0000000..7991bf4 --- /dev/null +++ b/tests/js_compat_test/payload.txt @@ -0,0 +1 @@ +payload-from-lib diff --git a/tests/js_compat_test/run_binary.sh b/tests/js_compat_test/run_binary.sh new file mode 100755 index 0000000..824bad7 --- /dev/null +++ b/tests/js_compat_test/run_binary.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +binary="$1" +output="$("${binary}")" + +if [[ ${output} != "helper:payload-from-lib compat-mode" ]]; then + echo "unexpected output: ${output}" >&2 + exit 1 +fi diff --git a/tests/js_compat_test/run_devserver.sh b/tests/js_compat_test/run_devserver.sh new file mode 100755 index 0000000..4e9bd4b --- /dev/null +++ b/tests/js_compat_test/run_devserver.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +binary="$1" +output="$("${binary}")" + +if [[ ${output} != "helper:payload-from-lib compat-mode devserver-mode" ]]; then + echo "unexpected output: ${output}" >&2 + exit 1 +fi diff --git a/tests/npm_compat_test/npm_translate_lock_workspace_test.sh b/tests/npm_compat_test/npm_translate_lock_workspace_test.sh new file mode 100755 index 0000000..fed8313 --- /dev/null +++ b/tests/npm_compat_test/npm_translate_lock_workspace_test.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +nix_cmd="${NIX:-/nix/var/nix/profiles/default/bin/nix}" +if [[ ! -x ${nix_cmd} ]]; then + nix_cmd="$(command -v nix || true)" +fi +if [[ -z ${nix_cmd} || ! -x ${nix_cmd} ]]; then + echo "nix is required to launch bazel from the repo dev shell" >&2 + exit 1 +fi + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +rules_bun_root="$(cd "${script_dir}/../.." && pwd -P)" + +workdir="$(mktemp -d)" +trap 'rm -rf "${workdir}"' EXIT + +fixture_dir="${workdir}/fixture" +mkdir -p "${fixture_dir}" + +cat >"${fixture_dir}/package.json" <<'JSON' +{ + "name": "npm-compat-test", + "type": "module", + "dependencies": { + "is-number": "7.0.0" + } +} +JSON + +cat >"${fixture_dir}/main.js" <<'JS' +import isNumber from "is-number"; + +console.log(`compat:${isNumber(42)}`); +JS + +( + cd "${rules_bun_root}" && + "${nix_cmd}" develop -c bash -lc 'bun install --cwd "$1" >/dev/null' bash "${fixture_dir}" +) +rm -rf "${fixture_dir}/node_modules" + +cat >"${fixture_dir}/MODULE.bazel" <"${fixture_dir}/BUILD.bazel" <<'EOF' +load("@npm//:defs.bzl", "npm_link_all_packages") +load("@rules_bun//js:defs.bzl", "js_binary") + +exports_files([ + "bun.lock", + "main.js", + "package.json", +]) + +npm_link_all_packages() + +js_binary( + name = "app", + entry_point = "main.js", + node_modules = ":node_modules", +) +EOF + +output="$( + cd "${rules_bun_root}" && + "${nix_cmd}" develop -c bash -lc 'cd "$1" && bazel run //:app' bash "${fixture_dir}" +)" + +if [[ ${output} != *"compat:true"* ]]; then + echo "unexpected output: ${output}" >&2 + exit 1 +fi + +query_output="$( + cd "${rules_bun_root}" && + "${nix_cmd}" develop -c bash -lc 'cd "$1" && bazel query //:npm__is_number' bash "${fixture_dir}" +)" +if ! grep -Fxq "//:npm__is_number" <<<"${query_output}"; then + echo "expected npm_link_all_packages to create //:npm__is_number" >&2 + exit 1 +fi From a0bc998bd26e4fc7491ed4ab2775045a6f084e35 Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 15 Mar 2026 00:11:55 +0100 Subject: [PATCH 3/4] feat: new bun_build and bun_compile, extend bun_install --- MODULE.bazel.lock | 43 +- README.md | 37 +- bun/BUILD.bazel | 2 + bun/defs.bzl | 3 + bun/extensions.bzl | 27 + bun/repositories.bzl | 15 + docs/BUILD.bazel | 3 +- docs/bun_install.md | 41 ++ docs/index.md | 9 + docs/rules.md | 543 ++++++++++++++---- internal/BUILD.bazel | 48 +- internal/bun_binary.bzl | 56 +- internal/bun_build_support.bzl | 90 +++ internal/bun_bundle.bzl | 28 +- internal/bun_command.bzl | 84 +++ internal/bun_compile.bzl | 315 ++++++++++ internal/bun_dev.bzl | 59 +- internal/bun_install.bzl | 42 +- internal/bun_script.bzl | 85 ++- internal/bun_test.bzl | 166 +++++- tests/binary_test/BUILD.bazel | 22 + tests/binary_test/flag_probe.ts | 7 + tests/binary_test/preload.ts | 1 + tests/binary_test/run_binary.sh | 2 +- tests/binary_test/run_flag_binary.sh | 12 + tests/binary_test/runtime.env | 1 + .../binary_test/verify_runtime_flags_shape.sh | 8 + tests/bun_test_test/BUILD.bazel | 28 + tests/bun_test_test/cache_hit_shape.sh | 5 +- tests/bun_test_test/cache_miss_shape.sh | 3 +- tests/bun_test_test/configured_suite_shape.sh | 23 + tests/bun_test_test/junit_shape.sh | 4 +- tests/bun_test_test/preload.ts | 1 + tests/bun_test_test/test.env | 1 + tests/bundle_test/BUILD.bazel | 49 +- tests/bundle_test/cli.ts | 1 + tests/bundle_test/run_compiled_binary.sh | 10 + tests/bundle_test/site/index.html | 12 + tests/bundle_test/site/main.ts | 1 + tests/bundle_test/site/styles.css | 4 + tests/bundle_test/verify_bundle.sh | 4 +- tests/bundle_test/verify_minify.sh | 6 +- tests/bundle_test/verify_site_build.sh | 14 + tests/bundle_test/verify_site_build_meta.sh | 29 + tests/install_test/BUILD.bazel | 7 + tests/install_test/clean_install.sh | 2 +- tests/install_test/environment_shape.sh | 1 + tests/install_test/install_flags_shape.sh | 16 + .../examples_basic_e2e_build_test.sh | 4 +- tests/library_test/verify_bundle.sh | 2 +- tests/script_test/BUILD.bazel | 22 + tests/script_test/run_workspace_script.sh | 15 + tests/script_test/workspace_run/package.json | 7 + .../workspace_run/packages/pkg-a/package.json | 6 + .../workspace_run/packages/pkg-a/say.ts | 1 + .../workspace_run/packages/pkg-b/package.json | 6 + .../workspace_run/packages/pkg-b/say.ts | 1 + tests/toolchain_test/toolchain_version.sh | 2 +- 58 files changed, 1845 insertions(+), 191 deletions(-) create mode 100644 internal/bun_build_support.bzl create mode 100644 internal/bun_command.bzl create mode 100644 internal/bun_compile.bzl create mode 100644 tests/binary_test/flag_probe.ts create mode 100644 tests/binary_test/preload.ts create mode 100755 tests/binary_test/run_flag_binary.sh create mode 100644 tests/binary_test/runtime.env create mode 100755 tests/binary_test/verify_runtime_flags_shape.sh create mode 100755 tests/bun_test_test/configured_suite_shape.sh create mode 100644 tests/bun_test_test/preload.ts create mode 100644 tests/bun_test_test/test.env create mode 100644 tests/bundle_test/cli.ts create mode 100755 tests/bundle_test/run_compiled_binary.sh create mode 100644 tests/bundle_test/site/index.html create mode 100644 tests/bundle_test/site/main.ts create mode 100644 tests/bundle_test/site/styles.css create mode 100755 tests/bundle_test/verify_site_build.sh create mode 100755 tests/bundle_test/verify_site_build_meta.sh create mode 100755 tests/install_test/install_flags_shape.sh create mode 100755 tests/script_test/run_workspace_script.sh create mode 100644 tests/script_test/workspace_run/package.json create mode 100644 tests/script_test/workspace_run/packages/pkg-a/package.json create mode 100644 tests/script_test/workspace_run/packages/pkg-a/say.ts create mode 100644 tests/script_test/workspace_run/packages/pkg-b/package.json create mode 100644 tests/script_test/workspace_run/packages/pkg-b/say.ts diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 41c5815..c180aff 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -34,8 +34,9 @@ "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", - "https://bcr.bazel.build/modules/bazel_features/1.33.0/source.json": "13617db3930328c2cd2807a0f13d52ca870ac05f96db9668655113265147b2a6", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", + "https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080", + "https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", @@ -51,8 +52,8 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb", - "https://bcr.bazel.build/modules/buildozer/8.2.1/MODULE.bazel": "61e9433c574c2bd9519cad7fa66b9c1d2b8e8d5f3ae5d6528a2c2d26e68d874d", - "https://bcr.bazel.build/modules/buildozer/8.2.1/source.json": "7c33f6a26ee0216f85544b4bca5e9044579e0219b6898dd653f5fb449cf2e484", + "https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f", + "https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", @@ -112,8 +113,8 @@ "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c", "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37", - "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8", - "https://bcr.bazel.build/modules/rules_cc/0.2.14/source.json": "55d0a4587c5592fad350f6e698530f4faf0e7dd15e69d43f8d87e220c78bea54", + "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", + "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07", "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", @@ -192,7 +193,7 @@ "moduleExtensions": { "//bun:extensions.bzl%bun": { "general": { - "bzlTransitiveDigest": "mWoMIEcKvXURFuMv68yk2TPrpNykLSLygedENme3WrQ=", + "bzlTransitiveDigest": "lzOUyaXDbkH922ruNkkwEF2cnI4m0XpzrOti0qypwtA=", "usagesDigest": "/0BcCMA6AOzLhQaRK6DquxrCfpPHJUjSUaFz4zmQrsM=", "recordedInputs": [ "REPO_MAPPING:,bazel_tools bazel_tools" @@ -218,6 +219,26 @@ "build_file_content": "\nexports_files([\"bun-linux-aarch64/bun\"])\n\nfilegroup(\n name = \"bun\",\n srcs = [\"bun-linux-aarch64/bun\"],\n visibility = [\"//visibility:public\"],\n)\n" } }, + "bun_linux_x64_musl": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "urls": [ + "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-x64-musl.zip" + ], + "sha256": "48a6c32277d343db0148ce066336472ffd380358a4d26bb1329714742492d824", + "build_file_content": "\nexports_files([\"bun-linux-x64-musl/bun\"])\n\nfilegroup(\n name = \"bun\",\n srcs = [\"bun-linux-x64-musl/bun\"],\n visibility = [\"//visibility:public\"],\n)\n" + } + }, + "bun_linux_aarch64_musl": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "urls": [ + "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-linux-aarch64-musl.zip" + ], + "sha256": "d2c81365a2e529b78a42330d3a0056e8dbd7896b4a6782c8e392b6532141e34d", + "build_file_content": "\nexports_files([\"bun-linux-aarch64-musl/bun\"])\n\nfilegroup(\n name = \"bun\",\n srcs = [\"bun-linux-aarch64-musl/bun\"],\n visibility = [\"//visibility:public\"],\n)\n" + } + }, "bun_darwin_x64": { "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { @@ -247,6 +268,16 @@ "sha256": "7a77b3e245e2e26965c93089a4a1332e8a326d3364c89fae1d1fd99cdd3cd73d", "build_file_content": "\nexports_files([\"bun-windows-x64/bun.exe\"])\n\nfilegroup(\n name = \"bun\",\n srcs = [\"bun-windows-x64/bun.exe\"],\n visibility = [\"//visibility:public\"],\n)\n" } + }, + "bun_windows_aarch64": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "urls": [ + "https://github.com/oven-sh/bun/releases/download/bun-v1.3.10/bun-windows-aarch64.zip" + ], + "sha256": "6822f3aa7bd2be40fb94c194a1185aae1c6fade54ca4fc2efdc722e37f3257d2", + "build_file_content": "\nexports_files([\"bun-windows-aarch64/bun.exe\"])\n\nfilegroup(\n name = \"bun\",\n srcs = [\"bun-windows-aarch64/bun.exe\"],\n visibility = [\"//visibility:public\"],\n)\n" + } } } } diff --git a/README.md b/README.md index 12cdaaf..c0bb043 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Bun rules for [Bazel](https://bazel.build) -`rules_bun` provides Bazel rules for running, testing, bundling, and developing -JavaScript and TypeScript code with Bun. +`rules_bun` provides Bazel rules for running, testing, building, compiling, +bundling, and developing JavaScript and TypeScript code with Bun. ## Repository layout @@ -29,6 +29,8 @@ The public entrypoint for rule authors and users is `@rules_bun//bun:defs.bzl`. `rules_bun` exports these primary rules: - `bun_binary` +- `bun_build` +- `bun_compile` - `bun_bundle` - `bun_dev` - `bun_script` @@ -70,9 +72,12 @@ use_repo( bun_ext, "bun_linux_x64", "bun_linux_aarch64", + "bun_linux_x64_musl", + "bun_linux_aarch64_musl", "bun_darwin_x64", "bun_darwin_aarch64", "bun_windows_x64", + "bun_windows_aarch64", ) register_toolchains( @@ -155,6 +160,8 @@ bun_register_toolchains() load( "@rules_bun//bun:defs.bzl", "bun_binary", + "bun_build", + "bun_compile", "bun_bundle", "bun_dev", "bun_script", @@ -194,6 +201,32 @@ When `node_modules` is provided, executables from `node_modules/.bin` are added to `PATH`. This label typically comes from `bun_install`, which still produces a standard `node_modules/` directory. +### `bun_build` and `bun_compile` + +Use `bun_build` for general-purpose `bun build` output directories and +`bun_compile` for standalone executables built with `bun build --compile`. + +```starlark +load("@rules_bun//bun:defs.bzl", "bun_build", "bun_compile") + +bun_build( + name = "site", + entry_points = ["src/index.html"], + data = glob(["src/**"]), + splitting = True, + metafile = True, +) + +bun_compile( + name = "cli", + entry_point = "src/cli.ts", +) +``` + +`bun_build` exposes a directory output so Bun can emit HTML, CSS, assets, and +split chunks. `bun_compile` produces a single executable artifact and supports +explicit cross-compilation via `compile_executable`. + ### `bun_dev` for local development Use `bun_dev` for long-running watch or hot-reload development targets. diff --git a/bun/BUILD.bazel b/bun/BUILD.bazel index 645c682..9b8cd59 100644 --- a/bun/BUILD.bazel +++ b/bun/BUILD.bazel @@ -35,7 +35,9 @@ bzl_library( visibility = ["//visibility:public"], deps = [ ":toolchain_bzl", + "//internal:bun_build_support_bzl", "//internal:bun_binary_bzl", + "//internal:bun_compile_bzl", "//internal:bun_bundle_bzl", "//internal:bun_dev_bzl", "//internal:bun_script_bzl", diff --git a/bun/defs.bzl b/bun/defs.bzl index 4c0ccc7..1fc3db0 100644 --- a/bun/defs.bzl +++ b/bun/defs.bzl @@ -1,4 +1,5 @@ """Public API surface for Bun Bazel rules.""" +load("//internal:bun_compile.bzl", _bun_build = "bun_build", _bun_compile = "bun_compile") 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") @@ -11,6 +12,8 @@ load(":toolchain.bzl", _BunToolchainInfo = "BunToolchainInfo", _bun_toolchain = visibility("public") bun_binary = _bun_binary +bun_build = _bun_build +bun_compile = _bun_compile bun_bundle = _bun_bundle bun_dev = _bun_dev bun_script = _bun_script diff --git a/bun/extensions.bzl b/bun/extensions.bzl index 1b7f9b4..2009234 100644 --- a/bun/extensions.bzl +++ b/bun/extensions.bzl @@ -13,6 +13,16 @@ _BUN_ARCHIVES = { "asset": "bun-linux-aarch64.zip", "binary": "bun-linux-aarch64/bun", }, + "bun_linux_x64_musl": { + "sha256": "48a6c32277d343db0148ce066336472ffd380358a4d26bb1329714742492d824", + "asset": "bun-linux-x64-musl.zip", + "binary": "bun-linux-x64-musl/bun", + }, + "bun_linux_aarch64_musl": { + "sha256": "d2c81365a2e529b78a42330d3a0056e8dbd7896b4a6782c8e392b6532141e34d", + "asset": "bun-linux-aarch64-musl.zip", + "binary": "bun-linux-aarch64-musl/bun", + }, "bun_darwin_x64": { "sha256": "c1d90bf6140f20e572c473065dc6b37a4b036349b5e9e4133779cc642ad94323", "asset": "bun-darwin-x64.zip", @@ -28,6 +38,11 @@ _BUN_ARCHIVES = { "asset": "bun-windows-x64.zip", "binary": "bun-windows-x64/bun.exe", }, + "bun_windows_aarch64": { + "sha256": "6822f3aa7bd2be40fb94c194a1185aae1c6fade54ca4fc2efdc722e37f3257d2", + "asset": "bun-windows-aarch64.zip", + "binary": "bun-windows-aarch64/bun.exe", + }, } _BUN_GITHUB_RELEASE_URL_TEMPLATE = "https://github.com/oven-sh/bun/releases/download/bun-v{}/{}" @@ -62,6 +77,12 @@ _install = tag_class( "bun_lockfile": attr.label(mandatory = True), "install_inputs": attr.label_list(allow_files = True), "isolated_home": attr.bool(default = True), + "production": attr.bool(default = False), + "omit": attr.string_list(), + "linker": attr.string(), + "backend": attr.string(), + "ignore_scripts": attr.bool(default = False), + "install_flags": attr.string_list(), }, ) @@ -75,6 +96,12 @@ def _bun_install_impl(ctx): bun_lockfile = install.bun_lockfile, install_inputs = install.install_inputs, isolated_home = install.isolated_home, + production = install.production, + omit = install.omit, + linker = install.linker, + backend = install.backend, + ignore_scripts = install.ignore_scripts, + install_flags = install.install_flags, visible_repo_name = install.name, ) diff --git a/bun/repositories.bzl b/bun/repositories.bzl index b475d4a..d20a7bc 100644 --- a/bun/repositories.bzl +++ b/bun/repositories.bzl @@ -12,6 +12,16 @@ _BUN_ARCHIVES = { "asset": "bun-linux-aarch64.zip", "binary": "bun-linux-aarch64/bun", }, + "bun_linux_x64_musl": { + "sha256": "48a6c32277d343db0148ce066336472ffd380358a4d26bb1329714742492d824", + "asset": "bun-linux-x64-musl.zip", + "binary": "bun-linux-x64-musl/bun", + }, + "bun_linux_aarch64_musl": { + "sha256": "d2c81365a2e529b78a42330d3a0056e8dbd7896b4a6782c8e392b6532141e34d", + "asset": "bun-linux-aarch64-musl.zip", + "binary": "bun-linux-aarch64-musl/bun", + }, "bun_darwin_x64": { "sha256": "c1d90bf6140f20e572c473065dc6b37a4b036349b5e9e4133779cc642ad94323", "asset": "bun-darwin-x64.zip", @@ -27,6 +37,11 @@ _BUN_ARCHIVES = { "asset": "bun-windows-x64.zip", "binary": "bun-windows-x64/bun.exe", }, + "bun_windows_aarch64": { + "sha256": "6822f3aa7bd2be40fb94c194a1185aae1c6fade54ca4fc2efdc722e37f3257d2", + "asset": "bun-windows-aarch64.zip", + "binary": "bun-windows-aarch64/bun.exe", + }, } _BUN_GITHUB_RELEASE_URL_TEMPLATE = "https://github.com/oven-sh/bun/releases/download/bun-v{}/{}" diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 76dccf4..7405060 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -8,6 +8,8 @@ stardoc( input = "//bun:defs.bzl", symbol_names = [ "bun_binary", + "bun_build", + "bun_compile", "bun_bundle", "bun_dev", "bun_script", @@ -19,5 +21,4 @@ stardoc( "ts_library", ], deps = ["//bun:defs_bzl"], - target_compatible_with = ["@platforms//os:linux"], ) diff --git a/docs/bun_install.md b/docs/bun_install.md index 4d2dab0..209d264 100644 --- a/docs/bun_install.md +++ b/docs/bun_install.md @@ -29,6 +29,8 @@ bun_install_ext.install( name = "bun_deps", package_json = "//:package.json", bun_lockfile = "//:bun.lock", + production = True, + omit = ["peer"], ) use_repo(bun_install_ext, "bun_deps") @@ -113,6 +115,45 @@ repository root. - `False`: lets Bun use the host `HOME`, which can improve repeated-install performance when Bun's cache is home-scoped +### `production` + +Optional boolean controlling whether Bun installs only production dependencies. + +Example: + +```starlark +production = True +``` + +### `omit` + +Optional list of dependency groups to omit, forwarded as repeated +`--omit` flags. Common values are `dev`, `optional`, and `peer`. + +### `linker` + +Optional Bun linker strategy, forwarded as `--linker`. + +Common values: + +- `isolated` +- `hoisted` + +### `backend` + +Optional Bun install backend, forwarded as `--backend`. + +Examples include `hardlink`, `symlink`, and `copyfile`. + +### `ignore_scripts` + +Optional boolean controlling whether Bun skips lifecycle scripts in the project +manifest. + +### `install_flags` + +Optional list of additional raw flags forwarded to `bun install`. + ## Notes - `bun_install` runs Bun, not npm. diff --git a/docs/index.md b/docs/index.md index d5a0a09..db90505 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,9 +36,12 @@ use_repo( bun_ext, "bun_linux_x64", "bun_linux_aarch64", + "bun_linux_x64_musl", + "bun_linux_aarch64_musl", "bun_darwin_x64", "bun_darwin_aarch64", "bun_windows_x64", + "bun_windows_aarch64", ) register_toolchains( @@ -79,6 +82,12 @@ bun_script( `bun_script` runs from the package directory by default and adds `node_modules/.bin` to `PATH`. +## Build and compile + +Use `bun_build` when Bun may emit a directory of outputs such as HTML, CSS, +chunks, and static assets. Use `bun_compile` for standalone executables created +with `bun build --compile`. + ## Regeneration The rule reference is generated from the public Starlark symbols in diff --git a/docs/rules.md b/docs/rules.md index 8aaf823..61c73b7 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -1,135 +1,484 @@ -# rules_bun rule reference + -This file documents the public rules exported from `@rules_bun//bun:defs.bzl`. +Public API surface for Bun Bazel rules. -## js_binary - -Runs a JS/TS entry point with Bun behind a `rules_js`-style name. - -Attributes: - -- `entry_point` (label, required): path to the main JS/TS file to execute. -- `node_modules` (label, optional): package files from a `node_modules` tree, typically produced by `bun_install` or `npm_translate_lock`, made available in runfiles. -- `data` (label_list, optional): additional runtime files. -- `deps` (label_list, optional): library dependencies required by the program. -- `args` (string_list, optional): default arguments appended before command-line arguments passed to the binary. -- `working_dir` (string, default: `"workspace"`, values: `"workspace" | "entry_point"`): runtime working directory. - -## js_test - -Runs Bun tests behind a `rules_js`-style name. - -Attributes: - -- `srcs` (label_list, required): test source files passed to `bun test`. -- `node_modules` (label, optional): package files from a `node_modules` tree, typically produced by `bun_install` or `npm_translate_lock`, made available in runfiles. -- `deps` (label_list, optional): library dependencies required by tests. -- `data` (label_list, optional): additional runtime files needed by tests. -- `args` (string_list, optional): default arguments appended after the test source list. - -## js_run_devserver - -Runs an executable target from a staged JS workspace. - -Attributes: - -- `tool` (label, required): executable target to launch as the dev server. -- `args` (string_list, optional): default arguments appended before command-line arguments passed to the dev server. -- `package_json` (label, optional): package manifest used to resolve the package working directory. -- `package_dir_hint` (string, default: `"."`): package-relative directory hint when `package_json` is omitted. -- `node_modules` (label, optional): package files from a `node_modules` tree, typically produced by `bun_install` or `npm_translate_lock`, made available in runfiles. -- `deps` (label_list, optional): library dependencies required by the dev server. -- `data` (label_list, optional): additional runtime files. -- `working_dir` (string, default: `"workspace"`, values: `"workspace" | "package"`): runtime working directory. + ## bun_binary -Runs a JS/TS entry point with Bun as an executable target (`bazel run`). +
+load("@rules_bun//bun:defs.bzl", "bun_binary")
 
-Attributes:
+bun_binary(name, deps, data, conditions, entry_point, env_files, install_mode, no_env_file,
+           node_modules, preload, run_flags, smol, working_dir)
+
-- `entry_point` (label, required): path to the main JS/TS file to execute. -- `node_modules` (label, optional): package files from a `node_modules` tree, typically produced by `bun_install`, made available in runfiles. -- `data` (label_list, optional): additional runtime files. -- `deps` (label_list, optional): library dependencies required by the program. -- `args` (string_list, optional): default arguments appended before command-line arguments passed to the binary. -- `working_dir` (string, default: `"workspace"`, values: `"workspace" | "entry_point"`): runtime working directory. +Runs a JS/TS entry point with Bun as an executable target. -## bun_dev +Use this rule for non-test scripts and CLIs that should run via `bazel run`. -Runs a JS/TS entry point in Bun development watch mode (`bazel run`). +**ATTRIBUTES** -Attributes: -- `entry_point` (label, required): path to the main JS/TS file. -- `watch_mode` (string, default: `"watch"`, values: `"watch" | "hot"`): Bun live-reload mode. -- `restart_on` (label_list, optional): files that trigger full process restart when changed. -- `node_modules` (label, optional): package files from a `node_modules` tree, typically produced by `bun_install`, made available in runfiles. -- `data` (label_list, optional): additional runtime files for dev process. -- `working_dir` (string, default: `"workspace"`, values: `"workspace" | "entry_point"`): runtime working directory. +| Name | Description | Type | Mandatory | Default | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| deps | Library dependencies required by the program. | List of labels | optional | `[]` | +| data | Additional runtime files required by the program. | List of labels | optional | `[]` | +| conditions | Custom package resolve conditions passed to Bun. | List of strings | optional | `[]` | +| entry_point | Path to the main JS/TS file to execute. | Label | required | | +| env_files | Additional environment files loaded with `--env-file`. | List of labels | optional | `[]` | +| install_mode | Whether Bun may auto-install missing packages at runtime. | String | optional | `"disable"` | +| no_env_file | If true, disables Bun's automatic `.env` loading. | Boolean | optional | `False` | +| node_modules | Optional label providing package files from a `node_modules` tree, typically produced by `bun_install`, in runfiles. | Label | optional | `None` | +| preload | Modules to preload with `--preload` before running the entry point. | List of labels | optional | `[]` | +| run_flags | Additional raw flags forwarded to `bun run` before the entry point. | List of strings | optional | `[]` | +| smol | If true, enables Bun's lower-memory runtime mode. | Boolean | optional | `False` | +| working_dir | Working directory at runtime: `workspace` root or nearest `entry_point` ancestor containing `.env`/`package.json`. | String | optional | `"workspace"` | -## bun_script -Runs a named `package.json` script with Bun as an executable target (`bazel run`). + -Recommended for package-script based tools such as Vite (`dev`, `build`, `preview`). -When `node_modules` is provided, executables from `node_modules/.bin` are added -to `PATH`, so scripts like `vite` work without wrapper scripts. +## bun_build -Attributes: +
+load("@rules_bun//bun:defs.bzl", "bun_build")
 
-- `script` (string, required): package script name passed to `bun run 
+  
+
diff --git a/tests/bundle_test/site/main.ts b/tests/bundle_test/site/main.ts
new file mode 100644
index 0000000..3ef72b5
--- /dev/null
+++ b/tests/bundle_test/site/main.ts
@@ -0,0 +1 @@
+document.getElementById("app")?.setAttribute("data-built", "yes");
diff --git a/tests/bundle_test/site/styles.css b/tests/bundle_test/site/styles.css
new file mode 100644
index 0000000..b780b02
--- /dev/null
+++ b/tests/bundle_test/site/styles.css
@@ -0,0 +1,4 @@
+body {
+  background: #f5efe2;
+  color: #1f1b14;
+}
diff --git a/tests/bundle_test/verify_bundle.sh b/tests/bundle_test/verify_bundle.sh
index b0122f9..58195ae 100755
--- a/tests/bundle_test/verify_bundle.sh
+++ b/tests/bundle_test/verify_bundle.sh
@@ -3,12 +3,12 @@ set -euo pipefail
 
 bundle="$1"
 
-if [[ ! -f "${bundle}" ]]; then
+if [[ ! -f ${bundle} ]]; then
   echo "Bundle output not found: ${bundle}" >&2
   exit 1
 fi
 
-if [[ ! -s "${bundle}" ]]; then
+if [[ ! -s ${bundle} ]]; then
   echo "Bundle output is empty: ${bundle}" >&2
   exit 1
 fi
diff --git a/tests/bundle_test/verify_minify.sh b/tests/bundle_test/verify_minify.sh
index eab37d7..6ccad26 100755
--- a/tests/bundle_test/verify_minify.sh
+++ b/tests/bundle_test/verify_minify.sh
@@ -4,10 +4,10 @@ set -euo pipefail
 bundle="$1"
 minified="$2"
 
-bundle_size="$(wc -c < "${bundle}")"
-minified_size="$(wc -c < "${minified}")"
+bundle_size="$(wc -c <"${bundle}")"
+minified_size="$(wc -c <"${minified}")"
 
-if (( minified_size >= bundle_size )); then
+if ((minified_size >= bundle_size)); then
   echo "Expected minified bundle (${minified_size}) to be smaller than regular bundle (${bundle_size})" >&2
   exit 1
 fi
diff --git a/tests/bundle_test/verify_site_build.sh b/tests/bundle_test/verify_site_build.sh
new file mode 100755
index 0000000..e3787de
--- /dev/null
+++ b/tests/bundle_test/verify_site_build.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+output_dir="$1"
+
+if [[ ! -d ${output_dir} ]]; then
+  echo "Expected output directory: ${output_dir}" >&2
+  exit 1
+fi
+
+if ! find -L "${output_dir}" -type f \( -name '*.js' -o -name '*.css' \) | grep -q .; then
+  echo "Expected Bun build assets in ${output_dir}" >&2
+  exit 1
+fi
diff --git a/tests/bundle_test/verify_site_build_meta.sh b/tests/bundle_test/verify_site_build_meta.sh
new file mode 100755
index 0000000..e0779f6
--- /dev/null
+++ b/tests/bundle_test/verify_site_build_meta.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+output_dir=""
+meta_json=""
+meta_md=""
+
+for path in "$@"; do
+  case "${path}" in
+  *.meta.json) meta_json="${path}" ;;
+  *.meta.md) meta_md="${path}" ;;
+  *) output_dir="${path}" ;;
+  esac
+done
+
+if [[ ! -d ${output_dir} ]]; then
+  echo "Expected directory output, got: ${output_dir}" >&2
+  exit 1
+fi
+
+if [[ ! -f ${meta_json} ]]; then
+  echo "Expected JSON metafile output" >&2
+  exit 1
+fi
+
+if [[ ! -f ${meta_md} ]]; then
+  echo "Expected markdown metafile output" >&2
+  exit 1
+fi
diff --git a/tests/install_test/BUILD.bazel b/tests/install_test/BUILD.bazel
index c3c47b4..749b29a 100644
--- a/tests/install_test/BUILD.bazel
+++ b/tests/install_test/BUILD.bazel
@@ -121,3 +121,10 @@ sh_test(
         "//conditions:default": ["@bun_linux_x64//:bun"],
     }),
 )
+
+sh_test(
+    name = "bun_install_install_flags_shape_test",
+    srcs = ["install_flags_shape.sh"],
+    args = ["$(location //internal:bun_install.bzl)"],
+    data = ["//internal:bun_install.bzl"],
+)
diff --git a/tests/install_test/clean_install.sh b/tests/install_test/clean_install.sh
index 93647f9..0eb37e1 100755
--- a/tests/install_test/clean_install.sh
+++ b/tests/install_test/clean_install.sh
@@ -5,7 +5,7 @@ bun_path="$1"
 workdir="$(mktemp -d)"
 trap 'rm -rf "${workdir}"' EXIT
 
-cat > "${workdir}/package.json" <<'JSON'
+cat >"${workdir}/package.json" <<'JSON'
 {
   "name": "clean-install-test",
   "version": "1.0.0"
diff --git a/tests/install_test/environment_shape.sh b/tests/install_test/environment_shape.sh
index 09b17f0..27d5fb1 100755
--- a/tests/install_test/environment_shape.sh
+++ b/tests/install_test/environment_shape.sh
@@ -7,3 +7,4 @@ grep -Eq 'install_args = \[str\(bun_bin\), "--bun", "install", "--frozen-lockfil
 grep -Eq 'if repository_ctx\.attr\.isolated_home:' "${rule_file}"
 grep -Eq 'environment[[:space:]]*=[[:space:]]*\{"HOME":[[:space:]]*str\(repository_ctx\.path\("\."\)\)\}' "${rule_file}"
 grep -Eq '"isolated_home": attr\.bool\(default = True\)' "${rule_file}"
+grep -Eq '"install_flags": attr\.string_list\(\)' "${rule_file}"
diff --git a/tests/install_test/install_flags_shape.sh b/tests/install_test/install_flags_shape.sh
new file mode 100755
index 0000000..8470b8b
--- /dev/null
+++ b/tests/install_test/install_flags_shape.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+rule_file="$1"
+
+grep -Fq 'repository_ctx.attr.production' "${rule_file}"
+grep -Fq '"--production"' "${rule_file}"
+grep -Fq 'for omit in repository_ctx.attr.omit' "${rule_file}"
+grep -Fq '"--omit"' "${rule_file}"
+grep -Fq 'repository_ctx.attr.linker' "${rule_file}"
+grep -Fq '"--linker"' "${rule_file}"
+grep -Fq 'repository_ctx.attr.backend' "${rule_file}"
+grep -Fq '"--backend"' "${rule_file}"
+grep -Fq 'repository_ctx.attr.ignore_scripts' "${rule_file}"
+grep -Fq '"--ignore-scripts"' "${rule_file}"
+grep -Fq 'repository_ctx.attr.install_flags' "${rule_file}"
diff --git a/tests/integration_test/examples_basic_e2e_build_test.sh b/tests/integration_test/examples_basic_e2e_build_test.sh
index 5a4108b..3d36399 100755
--- a/tests/integration_test/examples_basic_e2e_build_test.sh
+++ b/tests/integration_test/examples_basic_e2e_build_test.sh
@@ -4,6 +4,6 @@ set -euo pipefail
 build_file="$1"
 readme_file="$2"
 
-[[ -f "${build_file}" ]]
-[[ -f "${readme_file}" ]]
+[[ -f ${build_file} ]]
+[[ -f ${readme_file} ]]
 grep -Eq '^package\(default_visibility = \["//visibility:public"\]\)$' "${build_file}"
diff --git a/tests/library_test/verify_bundle.sh b/tests/library_test/verify_bundle.sh
index 006fd97..d33d867 100755
--- a/tests/library_test/verify_bundle.sh
+++ b/tests/library_test/verify_bundle.sh
@@ -3,7 +3,7 @@ set -euo pipefail
 
 bundle="$1"
 
-if [[ ! -s "${bundle}" ]]; then
+if [[ ! -s ${bundle} ]]; then
   echo "Expected bundled output to exist and be non-empty: ${bundle}" >&2
   exit 1
 fi
diff --git a/tests/script_test/BUILD.bazel b/tests/script_test/BUILD.bazel
index b17d41a..9336e2e 100644
--- a/tests/script_test/BUILD.bazel
+++ b/tests/script_test/BUILD.bazel
@@ -143,3 +143,25 @@ sh_test(
         ":paraglide_monorepo_app_b_build",
     ],
 )
+
+bun_script(
+    name = "workspace_filtered_script",
+    script = "say",
+    package_json = "workspace_run/package.json",
+    data = [
+        "workspace_run/packages/pkg-a/package.json",
+        "workspace_run/packages/pkg-a/say.ts",
+        "workspace_run/packages/pkg-b/package.json",
+        "workspace_run/packages/pkg-b/say.ts",
+    ],
+    filters = ["./packages/pkg-a"],
+    execution_mode = "sequential",
+    silent = True,
+)
+
+sh_test(
+    name = "bun_script_workspace_filter_test",
+    srcs = ["run_workspace_script.sh"],
+    args = ["$(location :workspace_filtered_script)"],
+    data = [":workspace_filtered_script"],
+)
diff --git a/tests/script_test/run_workspace_script.sh b/tests/script_test/run_workspace_script.sh
new file mode 100755
index 0000000..691bbff
--- /dev/null
+++ b/tests/script_test/run_workspace_script.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+script_bin="$1"
+output="$(${script_bin})"
+
+if [[ ${output} != *"pkg-a"* ]]; then
+  echo "Expected workspace run output to include pkg-a: ${output}" >&2
+  exit 1
+fi
+
+if [[ ${output} == *"pkg-b"* ]]; then
+  echo "Workspace filter unexpectedly included pkg-b: ${output}" >&2
+  exit 1
+fi
diff --git a/tests/script_test/workspace_run/package.json b/tests/script_test/workspace_run/package.json
new file mode 100644
index 0000000..2909d1d
--- /dev/null
+++ b/tests/script_test/workspace_run/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "workspace-run-root",
+  "private": true,
+  "workspaces": [
+    "packages/*"
+  ]
+}
diff --git a/tests/script_test/workspace_run/packages/pkg-a/package.json b/tests/script_test/workspace_run/packages/pkg-a/package.json
new file mode 100644
index 0000000..92faa4b
--- /dev/null
+++ b/tests/script_test/workspace_run/packages/pkg-a/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "pkg-a",
+  "scripts": {
+    "say": "bun ./say.ts"
+  }
+}
diff --git a/tests/script_test/workspace_run/packages/pkg-a/say.ts b/tests/script_test/workspace_run/packages/pkg-a/say.ts
new file mode 100644
index 0000000..9f02f95
--- /dev/null
+++ b/tests/script_test/workspace_run/packages/pkg-a/say.ts
@@ -0,0 +1 @@
+console.log("pkg-a");
diff --git a/tests/script_test/workspace_run/packages/pkg-b/package.json b/tests/script_test/workspace_run/packages/pkg-b/package.json
new file mode 100644
index 0000000..88dfd34
--- /dev/null
+++ b/tests/script_test/workspace_run/packages/pkg-b/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "pkg-b",
+  "scripts": {
+    "say": "bun ./say.ts"
+  }
+}
diff --git a/tests/script_test/workspace_run/packages/pkg-b/say.ts b/tests/script_test/workspace_run/packages/pkg-b/say.ts
new file mode 100644
index 0000000..aee7da0
--- /dev/null
+++ b/tests/script_test/workspace_run/packages/pkg-b/say.ts
@@ -0,0 +1 @@
+console.log("pkg-b");
diff --git a/tests/toolchain_test/toolchain_version.sh b/tests/toolchain_test/toolchain_version.sh
index 3024b9e..73e1387 100755
--- a/tests/toolchain_test/toolchain_version.sh
+++ b/tests/toolchain_test/toolchain_version.sh
@@ -4,7 +4,7 @@ set -euo pipefail
 bun_path="$1"
 version="$(${bun_path} --version)"
 
-if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
+if [[ ! ${version} =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
   echo "Unexpected bun version output: ${version}" >&2
   exit 1
 fi

From b35f03872cd95037cceda728d2d863d92b0b0fea Mon Sep 17 00:00:00 2001
From: eric 
Date: Sun, 15 Mar 2026 00:59:58 +0100
Subject: [PATCH 4/4] test: add more tests

---
 BUILD.bazel                                   |   8 +
 MODULE.bazel.lock                             |  49 ++++--
 bun/BUILD.bazel                               |  13 ++
 examples/basic/BUILD.bazel                    |   8 +
 examples/basic/README.md                      |   9 +
 internal/BUILD.bazel                          |  21 +++
 internal/bun_binary.bzl                       |   1 +
 internal/bun_bundle.bzl                       |   2 -
 internal/bun_dev.bzl                          |   1 +
 internal/bun_script.bzl                       |   1 +
 internal/bun_test.bzl                         |   1 +
 internal/js_run_devserver.bzl                 |   1 +
 internal/workspace.bzl                        | 149 ++++++++++++++--
 js/BUILD.bazel                                |   9 +
 npm/BUILD.bazel                               |  10 ++
 result                                        |   1 -
 tests/binary_test/BUILD.bazel                 |  24 +++
 .../verify_configured_launcher_shape.sh       |  16 ++
 tests/binary_test/verify_data_shape.sh        |   2 +-
 tests/bun_test_test/BUILD.bazel               |  17 +-
 tests/bun_test_test/configured_suite_shape.sh |   3 +-
 tests/bundle_test/BUILD.bazel                 | 121 +++++++++++++
 tests/bundle_test/fake_cross_bun.bin          |   1 +
 tests/bundle_test/out.js                      |  11 ++
 tests/bundle_test/out.js.map                  |  10 ++
 tests/bundle_test/sourcemap_bundle__main.js   |  11 ++
 .../bundle_test/sourcemap_bundle__main.js.map |  10 ++
 tests/bundle_test/sourcemap_case/BUILD.bazel  |  13 ++
 tests/bundle_test/sourcemap_case/entry.ts     |   3 +
 tests/bundle_test/verify_external_shape.sh    |   4 +-
 tests/bundle_test/verify_flag_aquery.sh       | 162 ++++++++++++++++++
 tests/bundle_test/verify_sourcemap_shape.sh   |  60 +++++++
 tests/install_test/BUILD.bazel                |  24 +++
 tests/install_test/determinism.sh             |   3 +-
 tests/install_test/workspace_parity.sh        |  26 ++-
 tests/integration_test/BUILD.bazel            |   8 +
 .../examples_basic_hot_restart_shape_test.sh  |  11 ++
 tests/js_compat_test/BUILD.bazel              |  27 +++
 tests/js_compat_test/app/package.json         |   4 +
 .../js_compat_test/verify_workspace_shape.sh  |  13 ++
 tests/npm_compat_test/BUILD.bazel             |  59 +++++++
 .../npm_translate_lock_workspace_test.sh      |  27 ++-
 tests/script_test/BUILD.bazel                 |  51 ++++++
 tests/script_test/run_workspace_parallel.sh   |  15 ++
 tests/script_test/verify_launcher_flags.sh    |  12 ++
 45 files changed, 978 insertions(+), 54 deletions(-)
 delete mode 120000 result
 create mode 100755 tests/binary_test/verify_configured_launcher_shape.sh
 create mode 100644 tests/bundle_test/fake_cross_bun.bin
 create mode 100644 tests/bundle_test/out.js
 create mode 100644 tests/bundle_test/out.js.map
 create mode 100644 tests/bundle_test/sourcemap_bundle__main.js
 create mode 100644 tests/bundle_test/sourcemap_bundle__main.js.map
 create mode 100644 tests/bundle_test/sourcemap_case/BUILD.bazel
 create mode 100644 tests/bundle_test/sourcemap_case/entry.ts
 create mode 100755 tests/bundle_test/verify_flag_aquery.sh
 create mode 100755 tests/bundle_test/verify_sourcemap_shape.sh
 create mode 100755 tests/integration_test/examples_basic_hot_restart_shape_test.sh
 create mode 100644 tests/js_compat_test/app/package.json
 create mode 100755 tests/js_compat_test/verify_workspace_shape.sh
 create mode 100644 tests/npm_compat_test/BUILD.bazel
 create mode 100755 tests/script_test/run_workspace_parallel.sh
 create mode 100755 tests/script_test/verify_launcher_flags.sh

diff --git a/BUILD.bazel b/BUILD.bazel
index ffd0fb0..546f681 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -1 +1,9 @@
 package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "repo_runtime_files",
+    srcs = [
+        "BUILD.bazel",
+        "MODULE.bazel",
+    ],
+)
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index c180aff..5a2235f 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -34,9 +34,8 @@
     "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
     "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
     "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6",
+    "https://bcr.bazel.build/modules/bazel_features/1.33.0/source.json": "13617db3930328c2cd2807a0f13d52ca870ac05f96db9668655113265147b2a6",
     "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
-    "https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080",
-    "https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309",
     "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
     "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
     "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
@@ -52,8 +51,8 @@
     "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
     "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
     "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
-    "https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f",
-    "https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd",
+    "https://bcr.bazel.build/modules/buildozer/8.2.1/MODULE.bazel": "61e9433c574c2bd9519cad7fa66b9c1d2b8e8d5f3ae5d6528a2c2d26e68d874d",
+    "https://bcr.bazel.build/modules/buildozer/8.2.1/source.json": "7c33f6a26ee0216f85544b4bca5e9044579e0219b6898dd653f5fb449cf2e484",
     "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
     "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
     "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
@@ -113,8 +112,8 @@
     "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8",
     "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c",
     "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37",
-    "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84",
-    "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07",
+    "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8",
+    "https://bcr.bazel.build/modules/rules_cc/0.2.14/source.json": "55d0a4587c5592fad350f6e698530f4faf0e7dd15e69d43f8d87e220c78bea54",
     "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
     "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
     "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
@@ -284,7 +283,7 @@
     },
     "//bun:extensions.bzl%bun_install": {
       "general": {
-        "bzlTransitiveDigest": "eSFVebwDN61an1dp3505njvMKN961HH+iY2tK6fEBQQ=",
+        "bzlTransitiveDigest": "lzOUyaXDbkH922ruNkkwEF2cnI4m0XpzrOti0qypwtA=",
         "usagesDigest": "f9pNm3AOxJDZmpHhL2vrrCo23IW33im/l/VYCTW2BWM=",
         "recordedInputs": [
           "REPO_MAPPING:,bazel_tools bazel_tools"
@@ -296,7 +295,14 @@
               "package_json": "@@//tests/script_test:vite_app/package.json",
               "bun_lockfile": "@@//tests/script_test:vite_app/bun.lock",
               "install_inputs": [],
-              "isolated_home": true
+              "isolated_home": true,
+              "production": false,
+              "omit": [],
+              "linker": "",
+              "backend": "",
+              "ignore_scripts": false,
+              "install_flags": [],
+              "visible_repo_name": "script_test_vite_node_modules"
             }
           },
           "script_test_vite_monorepo_node_modules": {
@@ -305,7 +311,14 @@
               "package_json": "@@//tests/script_test:vite_monorepo/package.json",
               "bun_lockfile": "@@//tests/script_test:vite_monorepo/bun.lock",
               "install_inputs": [],
-              "isolated_home": true
+              "isolated_home": true,
+              "production": false,
+              "omit": [],
+              "linker": "",
+              "backend": "",
+              "ignore_scripts": false,
+              "install_flags": [],
+              "visible_repo_name": "script_test_vite_monorepo_node_modules"
             }
           },
           "script_test_paraglide_monorepo_node_modules": {
@@ -314,7 +327,14 @@
               "package_json": "@@//tests/script_test:paraglide_monorepo/package.json",
               "bun_lockfile": "@@//tests/script_test:paraglide_monorepo/bun.lock",
               "install_inputs": [],
-              "isolated_home": true
+              "isolated_home": true,
+              "production": false,
+              "omit": [],
+              "linker": "",
+              "backend": "",
+              "ignore_scripts": false,
+              "install_flags": [],
+              "visible_repo_name": "script_test_paraglide_monorepo_node_modules"
             }
           },
           "examples_vite_monorepo_node_modules": {
@@ -323,7 +343,14 @@
               "package_json": "@@//examples/vite_monorepo:package.json",
               "bun_lockfile": "@@//examples/vite_monorepo:bun.lock",
               "install_inputs": [],
-              "isolated_home": true
+              "isolated_home": true,
+              "production": false,
+              "omit": [],
+              "linker": "",
+              "backend": "",
+              "ignore_scripts": false,
+              "install_flags": [],
+              "visible_repo_name": "examples_vite_monorepo_node_modules"
             }
           }
         }
diff --git a/bun/BUILD.bazel b/bun/BUILD.bazel
index 9b8cd59..dbdc256 100644
--- a/bun/BUILD.bazel
+++ b/bun/BUILD.bazel
@@ -10,6 +10,19 @@ exports_files([
     "version.bzl",
 ])
 
+filegroup(
+    name = "repo_runtime_files",
+    srcs = [
+        "BUILD.bazel",
+        "defs.bzl",
+        "extensions.bzl",
+        "repositories.bzl",
+        "toolchain.bzl",
+        "version.bzl",
+    ],
+    visibility = ["//visibility:public"],
+)
+
 bzl_library(
     name = "toolchain_bzl",
     srcs = ["toolchain.bzl"],
diff --git a/examples/basic/BUILD.bazel b/examples/basic/BUILD.bazel
index b17b5c6..e76df4b 100644
--- a/examples/basic/BUILD.bazel
+++ b/examples/basic/BUILD.bazel
@@ -11,3 +11,11 @@ bun_dev(
     name = "web_dev",
     entry_point = "main.ts",
 )
+
+bun_dev(
+    name = "web_dev_hot_restart",
+    entry_point = "main.ts",
+    no_clear_screen = True,
+    restart_on = ["README.md"],
+    watch_mode = "hot",
+)
diff --git a/examples/basic/README.md b/examples/basic/README.md
index fc59106..78e243d 100644
--- a/examples/basic/README.md
+++ b/examples/basic/README.md
@@ -9,3 +9,12 @@ bazel run //examples/basic:web_dev
 ```
 
 This starts Bun in watch mode for `main.ts`.
+
+For the hot-reload launcher variant:
+
+```bash
+bazel run //examples/basic:web_dev_hot_restart
+```
+
+This starts Bun with `watch_mode = "hot"`, disables screen clearing, and wires
+`README.md` through `restart_on` to exercise the custom restart launcher path.
diff --git a/internal/BUILD.bazel b/internal/BUILD.bazel
index 8ac00ce..32cdaa3 100644
--- a/internal/BUILD.bazel
+++ b/internal/BUILD.bazel
@@ -18,6 +18,27 @@ exports_files([
     "workspace.bzl",
 ])
 
+filegroup(
+    name = "repo_runtime_files",
+    srcs = [
+        "BUILD.bazel",
+        "bun_binary.bzl",
+        "bun_build_support.bzl",
+        "bun_bundle.bzl",
+        "bun_command.bzl",
+        "bun_compile.bzl",
+        "bun_dev.bzl",
+        "bun_install.bzl",
+        "bun_script.bzl",
+        "bun_test.bzl",
+        "js_compat.bzl",
+        "js_library.bzl",
+        "js_run_devserver.bzl",
+        "workspace.bzl",
+    ],
+    visibility = ["//visibility:public"],
+)
+
 bzl_library(
     name = "bun_command_bzl",
     srcs = ["bun_command.bzl"],
diff --git a/internal/bun_binary.bzl b/internal/bun_binary.bzl
index f73e656..d160fca 100644
--- a/internal/bun_binary.bzl
+++ b/internal/bun_binary.bzl
@@ -40,6 +40,7 @@ exec "${bun_bin}" "${bun_args[@]}" "$@"
         is_executable = True,
         content = render_workspace_setup(
             bun_short_path = bun_bin.short_path,
+            install_metadata_short_path = workspace_info.install_metadata_file.short_path if workspace_info.install_metadata_file else "",
             primary_source_short_path = entry_point.short_path,
             working_dir_mode = ctx.attr.working_dir,
         ) + command,
diff --git a/internal/bun_bundle.bzl b/internal/bun_bundle.bzl
index cefbc2b..e417d0c 100644
--- a/internal/bun_bundle.bzl
+++ b/internal/bun_bundle.bzl
@@ -25,8 +25,6 @@ def _bun_bundle_impl(ctx):
         add_bun_build_common_flags(args, ctx.attr)
         args.add("--outfile")
         args.add(output.path)
-        if ctx.attr.sourcemap:
-            args.add("--sourcemap")
         args.add(entry.path)
 
         ctx.actions.run(
diff --git a/internal/bun_dev.bzl b/internal/bun_dev.bzl
index 3ffa480..edddbb0 100644
--- a/internal/bun_dev.bzl
+++ b/internal/bun_dev.bzl
@@ -127,6 +127,7 @@ done
         is_executable = True,
         content = render_workspace_setup(
             bun_short_path = bun_bin.short_path,
+            install_metadata_short_path = workspace_info.install_metadata_file.short_path if workspace_info.install_metadata_file else "",
             primary_source_short_path = entry_point.short_path,
             working_dir_mode = ctx.attr.working_dir,
         ) + command,
diff --git a/internal/bun_script.bzl b/internal/bun_script.bzl
index 1ae4d53..7178c31 100644
--- a/internal/bun_script.bzl
+++ b/internal/bun_script.bzl
@@ -53,6 +53,7 @@ exec "${bun_bin}" "${bun_args[@]}" "$@"
             package_dir_hint = package_json.dirname or ".",
             package_json_short_path = package_json.short_path,
             primary_source_short_path = package_json.short_path,
+            install_metadata_short_path = workspace_info.install_metadata_file.short_path if workspace_info.install_metadata_file else "",
             working_dir_mode = ctx.attr.working_dir,
         ) + command,
     )
diff --git a/internal/bun_test.bzl b/internal/bun_test.bzl
index 5835f2b..bc0d678 100644
--- a/internal/bun_test.bzl
+++ b/internal/bun_test.bzl
@@ -81,6 +81,7 @@ exec "${bun_bin}" "${bun_args[@]}" "$@"
         is_executable = True,
         content = render_workspace_setup(
             bun_short_path = bun_bin.short_path,
+            install_metadata_short_path = workspace_info.install_metadata_file.short_path if workspace_info.install_metadata_file else "",
             primary_source_short_path = primary_file.short_path,
             working_dir_mode = "workspace",
         ) + command,
diff --git a/internal/js_run_devserver.bzl b/internal/js_run_devserver.bzl
index 96211d9..2db538f 100644
--- a/internal/js_run_devserver.bzl
+++ b/internal/js_run_devserver.bzl
@@ -31,6 +31,7 @@ def _js_run_devserver_impl(ctx):
         is_executable = True,
         content = render_workspace_setup(
             bun_short_path = bun_bin.short_path,
+            install_metadata_short_path = workspace_info.install_metadata_file.short_path if workspace_info.install_metadata_file else "",
             primary_source_short_path = package_json.short_path if package_json else tool_default_info.files_to_run.executable.short_path,
             package_json_short_path = package_json.short_path if package_json else "",
             package_dir_hint = ctx.attr.package_dir_hint,
diff --git a/internal/workspace.bzl b/internal/workspace.bzl
index a7c9691..8544a37 100644
--- a/internal/workspace.bzl
+++ b/internal/workspace.bzl
@@ -29,6 +29,11 @@ if [[ -n "__PACKAGE_JSON_SHORT_PATH__" ]]; then
     package_json="${runfiles_dir}/_main/__PACKAGE_JSON_SHORT_PATH__"
 fi
 package_rel_dir_hint="__PACKAGE_DIR_HINT__"
+install_root_rel_dir_hint="__INSTALL_ROOT_REL_DIR__"
+install_metadata=""
+if [[ -n "__INSTALL_METADATA_SHORT_PATH__" ]]; then
+    install_metadata="${runfiles_dir}/_main/__INSTALL_METADATA_SHORT_PATH__"
+fi
 working_dir_mode="__WORKING_DIR_MODE__"
 
 normalize_rel_dir() {
@@ -111,6 +116,27 @@ find_working_rel_dir_for_path() {
     rel_dir_from_abs_path "$(dirname "${path}")"
 }
 
+strip_rel_prefix() {
+    local child
+    child="$(normalize_rel_dir "$1")"
+    local parent
+    parent="$(normalize_rel_dir "$2")"
+
+    if [[ "${parent}" == "." ]]; then
+        echo "${child}"
+        return 0
+    fi
+    if [[ "${child}" == "${parent}" ]]; then
+        echo "."
+        return 0
+    fi
+    if [[ "${child}" == "${parent}/"* ]]; then
+        echo "${child#"${parent}/"}"
+        return 0
+    fi
+    echo "${child}"
+}
+
 select_primary_node_modules() {
     local selected=""
     local fallback=""
@@ -250,6 +276,15 @@ stage_workspace_view() {
     materialize_directory_entries "${source_root}/${package_rel_dir}" "${destination_root}/${package_rel_dir}"
 }
 
+materialize_tree_contents() {
+    local source_root="$1"
+    local destination_root="$2"
+
+    rm -rf "${destination_root}"
+    mkdir -p "${destination_root}"
+    cp -RL "${source_root}/." "${destination_root}"
+}
+
 build_workspace_package_map() {
     local root="$1"
     local out="$2"
@@ -414,12 +449,18 @@ mirror_install_repo_workspace_node_modules() {
 build_runtime_path() {
     local workspace_dir="$1"
     local package_dir="$2"
+    local install_root_dir="$3"
     local entries=()
 
-    if [[ -d "${package_dir}/node_modules/.bin" ]]; then
-        entries+=("${package_dir}/node_modules/.bin")
+    if [[ -d "${install_root_dir}/node_modules/.bin" ]]; then
+        entries+=("${install_root_dir}/node_modules/.bin")
     fi
-    if [[ -d "${workspace_dir}/node_modules/.bin" && "${workspace_dir}/node_modules/.bin" != "${package_dir}/node_modules/.bin" ]]; then
+    if [[ -d "${package_dir}/node_modules/.bin" ]]; then
+        if [[ "${package_dir}/node_modules/.bin" != "${install_root_dir}/node_modules/.bin" ]]; then
+            entries+=("${package_dir}/node_modules/.bin")
+        fi
+    fi
+    if [[ -d "${workspace_dir}/node_modules/.bin" && "${workspace_dir}/node_modules/.bin" != "${package_dir}/node_modules/.bin" && "${workspace_dir}/node_modules/.bin" != "${install_root_dir}/node_modules/.bin" ]]; then
         entries+=("${workspace_dir}/node_modules/.bin")
     fi
     if [[ -n "${PATH:-}" ]]; then
@@ -481,8 +522,66 @@ resolve_execution_rel_dir() {
     esac
 }
 
+resolve_install_root_rel_dir() {
+    if [[ -n "${install_metadata}" && -f "${install_metadata}" ]]; then
+        local resolved_from_metadata=""
+        resolved_from_metadata="$(
+            python3 - "${install_metadata}" "${package_rel_dir}" <<'PY'
+import json
+import sys
+
+install_metadata_path = sys.argv[1]
+package_rel_dir = sys.argv[2]
+
+try:
+    with open(install_metadata_path, "r", encoding="utf-8") as install_metadata_file:
+        workspace_package_dirs = json.load(install_metadata_file).get("workspace_package_dirs", [])
+except Exception:
+    workspace_package_dirs = []
+
+normalized_package_rel_dir = package_rel_dir.strip("./") or "."
+matches = []
+for workspace_package_dir in workspace_package_dirs:
+    normalized_workspace_package_dir = workspace_package_dir.strip("./")
+    if not normalized_workspace_package_dir:
+        continue
+    if normalized_package_rel_dir == normalized_workspace_package_dir:
+        matches.append((len(normalized_workspace_package_dir), "."))
+        continue
+    suffix = "/" + normalized_workspace_package_dir
+    if normalized_package_rel_dir.endswith(suffix):
+        prefix = normalized_package_rel_dir[:-len(suffix)].strip("/") or "."
+        matches.append((len(normalized_workspace_package_dir), prefix))
+
+if matches:
+    matches.sort(reverse = True)
+    print(matches[0][1])
+PY
+        )"
+        if [[ -n "${resolved_from_metadata}" ]]; then
+            echo "${resolved_from_metadata}"
+            return 0
+        fi
+    fi
+    if [[ -n "${install_root_rel_dir_hint}" && "${install_root_rel_dir_hint}" != "." ]]; then
+        normalize_rel_dir "${install_root_rel_dir_hint}"
+        return 0
+    fi
+    if [[ -n "${package_json}" ]]; then
+        find_package_rel_dir_for_path "${package_json}"
+        return 0
+    fi
+    if [[ -n "${primary_source}" ]]; then
+        find_package_rel_dir_for_path "${primary_source}"
+        return 0
+    fi
+    echo "."
+}
+
 package_rel_dir="$(resolve_package_rel_dir)"
 execution_rel_dir="$(resolve_execution_rel_dir "${package_rel_dir}")"
+install_root_rel_dir="$(resolve_install_root_rel_dir)"
+package_rel_dir_in_install_root="$(strip_rel_prefix "${package_rel_dir}" "${install_root_rel_dir}")"
 
 runtime_workspace="$(mktemp -d)"
 cleanup_runtime_workspace() {
@@ -494,11 +593,31 @@ runtime_package_dir="${runtime_workspace}"
 if [[ "${package_rel_dir}" != "." ]]; then
     runtime_package_dir="${runtime_workspace}/${package_rel_dir}"
 fi
+runtime_install_root="${runtime_workspace}"
+if [[ "${install_root_rel_dir}" != "." ]]; then
+    runtime_install_root="${runtime_workspace}/${install_root_rel_dir}"
+fi
 runtime_exec_dir="${runtime_workspace}"
 if [[ "${execution_rel_dir}" != "." ]]; then
     runtime_exec_dir="${runtime_workspace}/${execution_rel_dir}"
 fi
 
+if [[ -n "${primary_source}" ]]; then
+    materialize_tree_contents "${workspace_root}/${package_rel_dir}" "${runtime_package_dir}"
+fi
+
+if [[ -n "${package_json}" ]]; then
+    materialize_tree_contents "${workspace_root}/${install_root_rel_dir}" "${runtime_install_root}"
+fi
+
+if [[ -n "${primary_source}" && "${primary_source}" == "${workspace_root}"* ]]; then
+    primary_source="${runtime_workspace}/$(rel_dir_from_abs_path "${primary_source}")"
+fi
+
+if [[ -n "${package_json}" && "${package_json}" == "${workspace_root}"* ]]; then
+    package_json="${runtime_workspace}/$(rel_dir_from_abs_path "${package_json}")"
+fi
+
 workspace_package_map="${runtime_workspace}/.rules_bun_workspace_packages.tsv"
 build_workspace_package_map "${runtime_workspace}" "${workspace_package_map}"
 
@@ -506,22 +625,23 @@ primary_node_modules="$(select_primary_node_modules)"
 install_repo_root=""
 if [[ -n "${primary_node_modules}" ]]; then
     install_repo_root="$(dirname "${primary_node_modules}")"
-    mirror_node_modules_dir "${primary_node_modules}" "${runtime_workspace}/node_modules"
+    mkdir -p "${runtime_install_root}"
+    mirror_node_modules_dir "${primary_node_modules}" "${runtime_install_root}/node_modules"
 fi
 
 if [[ -n "${install_repo_root}" ]]; then
-    resolved_install_node_modules="$(find_install_repo_node_modules "${install_repo_root}" "${package_rel_dir}" || true)"
+    resolved_install_node_modules="$(find_install_repo_node_modules "${install_repo_root}" "${package_rel_dir_in_install_root}" || true)"
     if [[ -n "${resolved_install_node_modules}" && "${resolved_install_node_modules}" != "${install_repo_root}/node_modules" ]]; then
         mirror_node_modules_dir "${resolved_install_node_modules}" "${runtime_package_dir}/node_modules"
     fi
-    mirror_install_repo_workspace_node_modules "${install_repo_root}" "${runtime_workspace}"
+    mirror_install_repo_workspace_node_modules "${install_repo_root}" "${runtime_install_root}"
 fi
 
-if [[ ! -e "${runtime_package_dir}/node_modules" && -e "${runtime_workspace}/node_modules" && "${runtime_package_dir}" != "${runtime_workspace}" ]]; then
-    ln -s "${runtime_workspace}/node_modules" "${runtime_package_dir}/node_modules"
+if [[ ! -e "${runtime_package_dir}/node_modules" && -e "${runtime_install_root}/node_modules" && "${runtime_package_dir}" != "${runtime_install_root}" ]]; then
+    ln -s "${runtime_install_root}/node_modules" "${runtime_package_dir}/node_modules"
 fi
 
-runtime_path="$(build_runtime_path "${runtime_workspace}" "${runtime_package_dir}")"
+runtime_path="$(build_runtime_path "${runtime_workspace}" "${runtime_package_dir}" "${runtime_install_root}")"
 if [[ -n "${runtime_path}" ]]; then
     export PATH="${runtime_path}"
 fi
@@ -632,7 +752,9 @@ def render_workspace_setup(
         working_dir_mode,
         primary_source_short_path = "",
         package_json_short_path = "",
-        package_dir_hint = "."):
+        package_dir_hint = ".",
+        install_root_rel_dir = ".",
+        install_metadata_short_path = ""):
     return _WORKSPACE_SETUP_TEMPLATE.replace("__BUN_SHORT_PATH__", bun_short_path).replace(
         "__PRIMARY_SOURCE_SHORT_PATH__",
         primary_source_short_path,
@@ -642,8 +764,13 @@ def render_workspace_setup(
     ).replace(
         "__PACKAGE_DIR_HINT__",
         package_dir_hint or ".",
+    ).replace(
+        "__INSTALL_ROOT_REL_DIR__",
+        install_root_rel_dir or ".",
+    ).replace(
+        "__INSTALL_METADATA_SHORT_PATH__",
+        install_metadata_short_path,
     ).replace(
         "__WORKING_DIR_MODE__",
         working_dir_mode,
     )
-
diff --git a/js/BUILD.bazel b/js/BUILD.bazel
index fb0947c..e2bec17 100644
--- a/js/BUILD.bazel
+++ b/js/BUILD.bazel
@@ -4,6 +4,15 @@ package(default_visibility = ["//visibility:public"])
 
 exports_files(["defs.bzl"])
 
+filegroup(
+    name = "repo_runtime_files",
+    srcs = [
+        "BUILD.bazel",
+        "defs.bzl",
+    ],
+    visibility = ["//visibility:public"],
+)
+
 bzl_library(
     name = "defs_bzl",
     srcs = ["defs.bzl"],
diff --git a/npm/BUILD.bazel b/npm/BUILD.bazel
index 7fbb71a..03dc760 100644
--- a/npm/BUILD.bazel
+++ b/npm/BUILD.bazel
@@ -7,6 +7,16 @@ exports_files([
     "repositories.bzl",
 ])
 
+filegroup(
+    name = "repo_runtime_files",
+    srcs = [
+        "BUILD.bazel",
+        "extensions.bzl",
+        "repositories.bzl",
+    ],
+    visibility = ["//visibility:public"],
+)
+
 bzl_library(
     name = "extensions_bzl",
     srcs = ["extensions.bzl"],
diff --git a/result b/result
deleted file mode 120000
index b202d3d..0000000
--- a/result
+++ /dev/null
@@ -1 +0,0 @@
-/nix/store/742k6q4hns9h1wj61y90glqwfmn2y7pa-release
\ No newline at end of file
diff --git a/tests/binary_test/BUILD.bazel b/tests/binary_test/BUILD.bazel
index c3cd85b..8b17aba 100644
--- a/tests/binary_test/BUILD.bazel
+++ b/tests/binary_test/BUILD.bazel
@@ -93,3 +93,27 @@ sh_test(
     args = ["$(location :runtime_flag_bin)"],
     data = [":runtime_flag_bin"],
 )
+
+bun_binary(
+    name = "configured_launcher_bin",
+    entry_point = "hello.ts",
+    node_modules = "@script_test_vite_node_modules//:node_modules",
+    smol = True,
+    conditions = [
+        "browser",
+        "development",
+    ],
+    install_mode = "force",
+    run_flags = [
+        "--hot",
+        "--console-depth",
+        "4",
+    ],
+)
+
+sh_test(
+    name = "bun_binary_configured_launcher_shape_test",
+    srcs = ["verify_configured_launcher_shape.sh"],
+    args = ["$(location :configured_launcher_bin)"],
+    data = [":configured_launcher_bin"],
+)
diff --git a/tests/binary_test/verify_configured_launcher_shape.sh b/tests/binary_test/verify_configured_launcher_shape.sh
new file mode 100755
index 0000000..cc8e69f
--- /dev/null
+++ b/tests/binary_test/verify_configured_launcher_shape.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+binary="$1"
+
+grep -Fq -- 'install_metadata="${runfiles_dir}/_main/' "${binary}"
+grep -Fq -- 'node_modules/.rules_bun/install.json' "${binary}"
+grep -Fq -- "--smol" "${binary}"
+grep -Fq -- "--conditions" "${binary}"
+grep -Fq -- "'browser'" "${binary}"
+grep -Fq -- "'development'" "${binary}"
+grep -Fq -- "--install" "${binary}"
+grep -Fq -- "'force'" "${binary}"
+grep -Fq -- "'--hot'" "${binary}"
+grep -Fq -- "'--console-depth'" "${binary}"
+grep -Fq -- "'4'" "${binary}"
diff --git a/tests/binary_test/verify_data_shape.sh b/tests/binary_test/verify_data_shape.sh
index 22bcea5..74d5a5a 100755
--- a/tests/binary_test/verify_data_shape.sh
+++ b/tests/binary_test/verify_data_shape.sh
@@ -4,6 +4,6 @@ set -euo pipefail
 rule_file="$1"
 build_file="$2"
 
-grep -Eq 'files = \[bun_bin, entry_point\] \+ ctx\.files\.data' "${rule_file}"
+grep -Eq 'extra_files = ctx\.files\.data \+ ctx\.files\.preload \+ ctx\.files\.env_files \+ \[bun_bin\]' "${rule_file}"
 grep -Eq 'name = "hello_js_with_data_bin"' "${build_file}"
 grep -Eq 'data = \["payload\.txt"\]' "${build_file}"
diff --git a/tests/bun_test_test/BUILD.bazel b/tests/bun_test_test/BUILD.bazel
index 697f54d..ed0f912 100644
--- a/tests/bun_test_test/BUILD.bazel
+++ b/tests/bun_test_test/BUILD.bazel
@@ -20,7 +20,6 @@ bun_test(
     timeout_ms = 250,
     update_snapshots = True,
     rerun_each = 2,
-    retry = 3,
     concurrent = True,
     randomize = True,
     seed = 7,
@@ -32,6 +31,12 @@ bun_test(
     test_flags = ["--only-failures"],
 )
 
+bun_test(
+    name = "configured_retry_suite",
+    srcs = ["passing.test.ts"],
+    retry = 3,
+)
+
 sh_test(
     name = "bun_test_failing_suite_test",
     srcs = ["failing_suite_shape.sh"],
@@ -63,6 +68,12 @@ sh_test(
 sh_test(
     name = "bun_test_configured_suite_shape_test",
     srcs = ["configured_suite_shape.sh"],
-    args = ["$(location :configured_suite)"],
-    data = [":configured_suite"],
+    args = [
+        "$(location :configured_suite)",
+        "$(location :configured_retry_suite)",
+    ],
+    data = [
+        ":configured_suite",
+        ":configured_retry_suite",
+    ],
 )
diff --git a/tests/bun_test_test/configured_suite_shape.sh b/tests/bun_test_test/configured_suite_shape.sh
index af845b4..85bc65a 100755
--- a/tests/bun_test_test/configured_suite_shape.sh
+++ b/tests/bun_test_test/configured_suite_shape.sh
@@ -2,6 +2,7 @@
 set -euo pipefail
 
 launcher="$1"
+retry_launcher="$2"
 
 grep -Fq -- '--no-install' "${launcher}"
 grep -Fq -- '--preload' "${launcher}"
@@ -10,7 +11,6 @@ grep -Fq -- '--no-env-file' "${launcher}"
 grep -Fq -- '--timeout' "${launcher}"
 grep -Fq -- '--update-snapshots' "${launcher}"
 grep -Fq -- '--rerun-each' "${launcher}"
-grep -Fq -- '--retry' "${launcher}"
 grep -Fq -- '--concurrent' "${launcher}"
 grep -Fq -- '--randomize' "${launcher}"
 grep -Fq -- '--seed' "${launcher}"
@@ -21,3 +21,4 @@ grep -Fq -- '--reporter-outfile' "${launcher}"
 grep -Fq -- '--coverage' "${launcher}"
 grep -Fq -- '--coverage-dir' "${launcher}"
 grep -Fq -- '--coverage-reporter' "${launcher}"
+grep -Fq -- '--retry' "${retry_launcher}"
diff --git a/tests/bundle_test/BUILD.bazel b/tests/bundle_test/BUILD.bazel
index cdff097..6ac495b 100644
--- a/tests/bundle_test/BUILD.bazel
+++ b/tests/bundle_test/BUILD.bazel
@@ -39,11 +39,101 @@ bun_build(
     metafile_md = True,
 )
 
+bun_build(
+    name = "advanced_site_build",
+    tags = ["manual"],
+    entry_points = ["site/index.html"],
+    data = [
+        "site/main.ts",
+        "site/styles.css",
+    ],
+    install_mode = "fallback",
+    target = "node",
+    format = "cjs",
+    production = True,
+    splitting = True,
+    root = "tests/bundle_test/site",
+    sourcemap = "linked",
+    banner = "/* bundle banner */",
+    footer = "// bundle footer",
+    public_path = "/static/",
+    packages = "external",
+    external = [
+        "left-pad",
+        "react",
+    ],
+    entry_naming = "entries/[name]-[hash].[ext]",
+    chunk_naming = "chunks/[name]-[hash].[ext]",
+    asset_naming = "assets/[name]-[hash].[ext]",
+    minify = True,
+    minify_syntax = True,
+    minify_whitespace = True,
+    minify_identifiers = True,
+    keep_names = True,
+    css_chunking = True,
+    conditions = [
+        "browser",
+        "custom",
+    ],
+    env = "PUBLIC_*",
+    define = [
+        "process.env.NODE_ENV:\"production\"",
+        "__DEV__:false",
+    ],
+    drop = [
+        "console",
+        "debugger",
+    ],
+    feature = [
+        "react_fast_refresh",
+        "server_components",
+    ],
+    loader = [
+        ".svg:file",
+        ".txt:text",
+    ],
+    jsx_factory = "h",
+    jsx_fragment = "Fragment",
+    jsx_import_source = "preact",
+    jsx_runtime = "automatic",
+    jsx_side_effects = True,
+    react_fast_refresh = True,
+    emit_dce_annotations = True,
+    no_bundle = True,
+    build_flags = [
+        "--app",
+        "--server-components",
+    ],
+)
+
 bun_compile(
     name = "compiled_cli",
     entry_point = "cli.ts",
 )
 
+bun_compile(
+    name = "compiled_cli_with_flags",
+    tags = ["manual"],
+    entry_point = "cli.ts",
+    bytecode = True,
+    compile_exec_argv = [
+        "--smol",
+        "--inspect-wait",
+    ],
+    compile_executable = "fake_cross_bun.bin",
+    compile_autoload_dotenv = False,
+    compile_autoload_bunfig = False,
+    compile_autoload_tsconfig = True,
+    compile_autoload_package_json = True,
+    windows_hide_console = True,
+    windows_icon = "branding/icon.ico",
+    windows_title = "Rules Bun Test App",
+    windows_publisher = "rules_bun",
+    windows_version = "1.2.3.4",
+    windows_description = "compile flag coverage",
+    windows_copyright = "(c) rules_bun",
+)
+
 sh_test(
     name = "bundle_output_test",
     srcs = ["verify_bundle.sh"],
@@ -84,6 +174,20 @@ sh_test(
     ],
 )
 
+sh_test(
+    name = "bundle_sourcemap_shape_test",
+    srcs = ["verify_sourcemap_shape.sh"],
+    env_inherit = ["PATH"],
+    data = [
+        "//:repo_runtime_files",
+        "//bun:repo_runtime_files",
+        "//internal:repo_runtime_files",
+        "BUILD.bazel",
+        "//tests/bundle_test/sourcemap_case:BUILD.bazel",
+        "//tests/bundle_test/sourcemap_case:entry.ts",
+    ],
+)
+
 sh_test(
     name = "bun_build_site_output_test",
     srcs = ["verify_site_build.sh"],
@@ -104,3 +208,20 @@ sh_test(
     args = ["$(location :compiled_cli)"],
     data = [":compiled_cli"],
 )
+
+sh_test(
+    name = "bun_build_compile_flag_shape_test",
+    srcs = ["verify_flag_aquery.sh"],
+    env_inherit = ["PATH"],
+    data = [
+        "//:repo_runtime_files",
+        "//bun:repo_runtime_files",
+        "//internal:repo_runtime_files",
+        "BUILD.bazel",
+        "cli.ts",
+        "fake_cross_bun.bin",
+        "site/index.html",
+        "site/main.ts",
+        "site/styles.css",
+    ],
+)
diff --git a/tests/bundle_test/fake_cross_bun.bin b/tests/bundle_test/fake_cross_bun.bin
new file mode 100644
index 0000000..48cdce8
--- /dev/null
+++ b/tests/bundle_test/fake_cross_bun.bin
@@ -0,0 +1 @@
+placeholder
diff --git a/tests/bundle_test/out.js b/tests/bundle_test/out.js
new file mode 100644
index 0000000..ca4944e
--- /dev/null
+++ b/tests/bundle_test/out.js
@@ -0,0 +1,11 @@
+// tests/bundle_test/main.ts
+function greet(name) {
+  return `Hello ${name}`;
+}
+console.log(greet("bundle"));
+export {
+  greet
+};
+
+//# debugId=A86FEBA7FCC390B664756E2164756E21
+//# sourceMappingURL=out.js.map
diff --git a/tests/bundle_test/out.js.map b/tests/bundle_test/out.js.map
new file mode 100644
index 0000000..35ba836
--- /dev/null
+++ b/tests/bundle_test/out.js.map
@@ -0,0 +1,10 @@
+{
+  "version": 3,
+  "sources": ["tests/bundle_test/main.ts"],
+  "sourcesContent": [
+    "export function greet(name: string): string {\n  return `Hello ${name}`;\n}\n\nconsole.log(greet(\"bundle\"));\n"
+  ],
+  "mappings": ";AAAO,SAAS,KAAK,CAAC,MAAsB;AAAA,EAC1C,OAAO,SAAS;AAAA;AAGlB,QAAQ,IAAI,MAAM,QAAQ,CAAC;",
+  "debugId": "A86FEBA7FCC390B664756E2164756E21",
+  "names": []
+}
\ No newline at end of file
diff --git a/tests/bundle_test/sourcemap_bundle__main.js b/tests/bundle_test/sourcemap_bundle__main.js
new file mode 100644
index 0000000..344b5f2
--- /dev/null
+++ b/tests/bundle_test/sourcemap_bundle__main.js
@@ -0,0 +1,11 @@
+// ../../../../../../../Projects/rules_bun/tests/bundle_test/main.ts
+function greet(name) {
+  return `Hello ${name}`;
+}
+console.log(greet("bundle"));
+export {
+  greet
+};
+
+//# debugId=D8717FECBBDCEC7764756E2164756E21
+//# sourceMappingURL=sourcemap_bundle__main.js.map
diff --git a/tests/bundle_test/sourcemap_bundle__main.js.map b/tests/bundle_test/sourcemap_bundle__main.js.map
new file mode 100644
index 0000000..58d0301
--- /dev/null
+++ b/tests/bundle_test/sourcemap_bundle__main.js.map
@@ -0,0 +1,10 @@
+{
+  "version": 3,
+  "sources": ["../../../../../../../Projects/rules_bun/tests/bundle_test/main.ts"],
+  "sourcesContent": [
+    "export function greet(name: string): string {\n  return `Hello ${name}`;\n}\n\nconsole.log(greet(\"bundle\"));\n"
+  ],
+  "mappings": ";AAAO,SAAS,KAAK,CAAC,MAAsB;AAAA,EAC1C,OAAO,SAAS;AAAA;AAGlB,QAAQ,IAAI,MAAM,QAAQ,CAAC;",
+  "debugId": "D8717FECBBDCEC7764756E2164756E21",
+  "names": []
+}
\ No newline at end of file
diff --git a/tests/bundle_test/sourcemap_case/BUILD.bazel b/tests/bundle_test/sourcemap_case/BUILD.bazel
new file mode 100644
index 0000000..7600ec2
--- /dev/null
+++ b/tests/bundle_test/sourcemap_case/BUILD.bazel
@@ -0,0 +1,13 @@
+load("//bun:defs.bzl", "bun_bundle")
+
+exports_files([
+    "BUILD.bazel",
+    "entry.ts",
+])
+
+bun_bundle(
+    name = "sourcemap_bundle",
+    tags = ["manual"],
+    entry_points = ["entry.ts"],
+    sourcemap = True,
+)
diff --git a/tests/bundle_test/sourcemap_case/entry.ts b/tests/bundle_test/sourcemap_case/entry.ts
new file mode 100644
index 0000000..c4ac5c3
--- /dev/null
+++ b/tests/bundle_test/sourcemap_case/entry.ts
@@ -0,0 +1,3 @@
+const message: string = "sourcemap coverage";
+
+console.log(message);
diff --git a/tests/bundle_test/verify_external_shape.sh b/tests/bundle_test/verify_external_shape.sh
index cff7e1c..e0bef6d 100755
--- a/tests/bundle_test/verify_external_shape.sh
+++ b/tests/bundle_test/verify_external_shape.sh
@@ -4,7 +4,7 @@ set -euo pipefail
 rule_file="$1"
 build_file="$2"
 
-grep -Eq 'for package in ctx\.attr\.external:' "${rule_file}"
-grep -Eq 'args\.add\("--external"\)' "${rule_file}"
+grep -Eq 'add_bun_build_common_flags\(args, ctx\.attr\)' "${rule_file}"
+grep -Eq '"external": attr\.string_list\(' "${rule_file}"
 grep -Eq 'name = "external_bundle"' "${build_file}"
 grep -Eq 'external = \["left-pad"\]' "${build_file}"
diff --git a/tests/bundle_test/verify_flag_aquery.sh b/tests/bundle_test/verify_flag_aquery.sh
new file mode 100755
index 0000000..f9fc033
--- /dev/null
+++ b/tests/bundle_test/verify_flag_aquery.sh
@@ -0,0 +1,162 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if command -v bazel >/dev/null 2>&1; then
+  bazel_cmd=(bazel)
+elif command -v bazelisk >/dev/null 2>&1; then
+  bazel_cmd=(bazelisk)
+else
+  echo "bazel or bazelisk is required on PATH" >&2
+  exit 1
+fi
+
+find_workspace_root() {
+  local candidate
+  local module_path
+  local script_dir
+
+  for candidate in \
+    "${TEST_SRCDIR:-}/${TEST_WORKSPACE:-}" \
+    "${TEST_SRCDIR:-}/_main"; do
+    if [[ -n ${candidate} && -f "${candidate}/MODULE.bazel" ]]; then
+      printf '%s\n' "${candidate}"
+      return 0
+    fi
+  done
+
+  if [[ -n ${TEST_SRCDIR:-} ]]; then
+    module_path="$(find "${TEST_SRCDIR}" -maxdepth 3 -name MODULE.bazel -print -quit 2>/dev/null || true)"
+    if [[ -n ${module_path} ]]; then
+      dirname "${module_path}"
+      return 0
+    fi
+  fi
+
+  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
+  candidate="$(cd "${script_dir}/../.." && pwd -P)"
+  if [[ -f "${candidate}/MODULE.bazel" ]]; then
+    printf '%s\n' "${candidate}"
+    return 0
+  fi
+
+  echo "Unable to locate rules_bun workspace root" >&2
+  exit 1
+}
+
+rules_bun_root="$(find_workspace_root)"
+
+run_aquery() {
+  local mnemonic="$1"
+  local target="$2"
+
+  (
+    cd "${rules_bun_root}" &&
+      "${bazel_cmd[@]}" aquery "mnemonic(\"${mnemonic}\", ${target})" --output=textproto
+  )
+}
+
+expect_line() {
+  local output="$1"
+  local expected="$2"
+
+  if ! grep -Fq -- "${expected}" <<<"${output}"; then
+    echo "Expected aquery output to contain: ${expected}" >&2
+    exit 1
+  fi
+}
+
+build_output="$(run_aquery "BunBuild" "//tests/bundle_test:advanced_site_build")"
+
+for expected in \
+  'arguments: "--install"' \
+  'arguments: "fallback"' \
+  'arguments: "--target"' \
+  'arguments: "node"' \
+  'arguments: "--format"' \
+  'arguments: "cjs"' \
+  'arguments: "--production"' \
+  'arguments: "--splitting"' \
+  'arguments: "--root"' \
+  'arguments: "tests/bundle_test/site"' \
+  'arguments: "--sourcemap"' \
+  'arguments: "linked"' \
+  'arguments: "--banner"' \
+  'arguments: "/* bundle banner */"' \
+  'arguments: "--footer"' \
+  'arguments: "// bundle footer"' \
+  'arguments: "--public-path"' \
+  'arguments: "/static/"' \
+  'arguments: "--packages"' \
+  'arguments: "external"' \
+  'arguments: "left-pad"' \
+  'arguments: "react"' \
+  'arguments: "--entry-naming"' \
+  'arguments: "entries/[name]-[hash].[ext]"' \
+  'arguments: "--chunk-naming"' \
+  'arguments: "chunks/[name]-[hash].[ext]"' \
+  'arguments: "--asset-naming"' \
+  'arguments: "assets/[name]-[hash].[ext]"' \
+  'arguments: "--minify"' \
+  'arguments: "--minify-syntax"' \
+  'arguments: "--minify-whitespace"' \
+  'arguments: "--minify-identifiers"' \
+  'arguments: "--keep-names"' \
+  'arguments: "--css-chunking"' \
+  'arguments: "--conditions"' \
+  'arguments: "browser"' \
+  'arguments: "custom"' \
+  'arguments: "--env"' \
+  'arguments: "PUBLIC_*"' \
+  'arguments: "process.env.NODE_ENV:\"production\""' \
+  'arguments: "__DEV__:false"' \
+  'arguments: "console"' \
+  'arguments: "debugger"' \
+  'arguments: "react_fast_refresh"' \
+  'arguments: "server_components"' \
+  'arguments: ".svg:file"' \
+  'arguments: ".txt:text"' \
+  'arguments: "--jsx-factory"' \
+  'arguments: "h"' \
+  'arguments: "--jsx-fragment"' \
+  'arguments: "Fragment"' \
+  'arguments: "--jsx-import-source"' \
+  'arguments: "preact"' \
+  'arguments: "--jsx-runtime"' \
+  'arguments: "automatic"' \
+  'arguments: "--jsx-side-effects"' \
+  'arguments: "--react-fast-refresh"' \
+  'arguments: "--emit-dce-annotations"' \
+  'arguments: "--no-bundle"' \
+  'arguments: "--app"' \
+  'arguments: "--server-components"'; do
+  expect_line "${build_output}" "${expected}"
+done
+
+compile_output="$(run_aquery "BunCompile" "//tests/bundle_test:compiled_cli_with_flags")"
+
+for expected in \
+  'arguments: "--bytecode"' \
+  'arguments: "--compile-exec-argv"' \
+  'arguments: "--smol"' \
+  'arguments: "--inspect-wait"' \
+  'arguments: "--no-compile-autoload-dotenv"' \
+  'arguments: "--no-compile-autoload-bunfig"' \
+  'arguments: "--compile-autoload-tsconfig"' \
+  'arguments: "--compile-autoload-package-json"' \
+  'arguments: "--compile-executable-path"' \
+  'arguments: "tests/bundle_test/fake_cross_bun.bin"' \
+  'arguments: "--windows-hide-console"' \
+  'arguments: "--windows-icon"' \
+  'arguments: "branding/icon.ico"' \
+  'arguments: "--windows-title"' \
+  'arguments: "Rules Bun Test App"' \
+  'arguments: "--windows-publisher"' \
+  'arguments: "rules_bun"' \
+  'arguments: "--windows-version"' \
+  'arguments: "1.2.3.4"' \
+  'arguments: "--windows-description"' \
+  'arguments: "compile flag coverage"' \
+  'arguments: "--windows-copyright"' \
+  'arguments: "(c) rules_bun"'; do
+  expect_line "${compile_output}" "${expected}"
+done
diff --git a/tests/bundle_test/verify_sourcemap_shape.sh b/tests/bundle_test/verify_sourcemap_shape.sh
new file mode 100755
index 0000000..0d9f8d8
--- /dev/null
+++ b/tests/bundle_test/verify_sourcemap_shape.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if command -v bazel >/dev/null 2>&1; then
+  bazel_cmd=(bazel)
+elif command -v bazelisk >/dev/null 2>&1; then
+  bazel_cmd=(bazelisk)
+else
+  echo "bazel or bazelisk is required on PATH" >&2
+  exit 1
+fi
+
+find_workspace_root() {
+  local candidate
+  local module_path
+  local script_dir
+
+  for candidate in \
+    "${TEST_SRCDIR:-}/${TEST_WORKSPACE:-}" \
+    "${TEST_SRCDIR:-}/_main"; do
+    if [[ -n ${candidate} && -f "${candidate}/MODULE.bazel" ]]; then
+      printf '%s\n' "${candidate}"
+      return 0
+    fi
+  done
+
+  if [[ -n ${TEST_SRCDIR:-} ]]; then
+    module_path="$(find "${TEST_SRCDIR}" -maxdepth 3 -name MODULE.bazel -print -quit 2>/dev/null || true)"
+    if [[ -n ${module_path} ]]; then
+      dirname "${module_path}"
+      return 0
+    fi
+  fi
+
+  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
+  candidate="$(cd "${script_dir}/../.." && pwd -P)"
+  if [[ -f "${candidate}/MODULE.bazel" ]]; then
+    printf '%s\n' "${candidate}"
+    return 0
+  fi
+
+  echo "Unable to locate rules_bun workspace root" >&2
+  exit 1
+}
+
+rules_bun_root="$(find_workspace_root)"
+
+bundle_output="$(
+  cd "${rules_bun_root}" &&
+    "${bazel_cmd[@]}" aquery 'mnemonic("BunBundle", //tests/bundle_test/sourcemap_case:sourcemap_bundle)' --output=textproto
+)"
+
+count="$(grep -Fc 'arguments: "--sourcemap"' <<<"${bundle_output}")"
+if [[ ${count} != "1" ]]; then
+  echo "Expected bun_bundle(sourcemap = True) to emit exactly one --sourcemap flag, got ${count}" >&2
+  exit 1
+fi
+
+grep -Fq 'arguments: "--outfile"' <<<"${bundle_output}"
+grep -Fq 'arguments: "tests/bundle_test/sourcemap_case/entry.ts"' <<<"${bundle_output}"
diff --git a/tests/install_test/BUILD.bazel b/tests/install_test/BUILD.bazel
index 749b29a..8292a7d 100644
--- a/tests/install_test/BUILD.bazel
+++ b/tests/install_test/BUILD.bazel
@@ -122,6 +122,30 @@ sh_test(
     }),
 )
 
+sh_test(
+    name = "bun_install_workspace_parity_test",
+    srcs = ["workspace_parity.sh"],
+    env_inherit = ["PATH"],
+    args = select({
+        ":linux_x86_64": ["$(location @bun_linux_x64//:bun)"],
+        ":linux_aarch64": ["$(location @bun_linux_aarch64//:bun)"],
+        ":darwin_x86_64": ["$(location @bun_darwin_x64//:bun)"],
+        ":darwin_aarch64": ["$(location @bun_darwin_aarch64//:bun)"],
+        "//conditions:default": ["$(location @bun_linux_x64//:bun)"],
+    }),
+    data = select({
+        ":linux_x86_64": ["@bun_linux_x64//:bun"],
+        ":linux_aarch64": ["@bun_linux_aarch64//:bun"],
+        ":darwin_x86_64": ["@bun_darwin_x64//:bun"],
+        ":darwin_aarch64": ["@bun_darwin_aarch64//:bun"],
+        "//conditions:default": ["@bun_linux_x64//:bun"],
+    }) + [
+        "//:repo_runtime_files",
+        "//bun:repo_runtime_files",
+        "//internal:repo_runtime_files",
+    ],
+)
+
 sh_test(
     name = "bun_install_install_flags_shape_test",
     srcs = ["install_flags_shape.sh"],
diff --git a/tests/install_test/determinism.sh b/tests/install_test/determinism.sh
index fe382bd..904a353 100755
--- a/tests/install_test/determinism.sh
+++ b/tests/install_test/determinism.sh
@@ -4,10 +4,11 @@ set -euo pipefail
 rule_file="$1"
 
 grep -Eq 'install", "--frozen-lockfile", "--no-progress"' "${rule_file}"
-grep -Eq 'repository_ctx\.file\("package\.json", repository_ctx\.read\(package_json\)\)' "${rule_file}"
+grep -Eq 'repository_ctx\.file\("package\.json", _normalized_root_manifest\(repository_ctx, package_json\)\)' "${rule_file}"
 grep -Eq 'lockfile_name = bun_lockfile\.basename' "${rule_file}"
 grep -Eq 'if lockfile_name not in \["bun\.lock", "bun\.lockb"\]:' "${rule_file}"
 grep -Eq 'repository_ctx\.symlink\(bun_lockfile, lockfile_name\)' "${rule_file}"
 grep -Eq 'glob\(\["\*\*/node_modules/\*\*"\]' "${rule_file}"
 grep -Eq '_DEFAULT_INSTALL_INPUTS = \[' "${rule_file}"
 grep -Eq '"install_inputs": attr\.label_list\(allow_files = True\)' "${rule_file}"
+grep -Eq '_materialize_install_inputs\(repository_ctx, package_json\)' "${rule_file}"
diff --git a/tests/install_test/workspace_parity.sh b/tests/install_test/workspace_parity.sh
index 41b35f2..108fef4 100755
--- a/tests/install_test/workspace_parity.sh
+++ b/tests/install_test/workspace_parity.sh
@@ -3,8 +3,12 @@ set -euo pipefail
 
 bun_path="${1:-bun}"
 
-if ! command -v bazel >/dev/null 2>&1; then
-  echo "bazel is required on PATH" >&2
+if command -v bazel >/dev/null 2>&1; then
+  bazel_cmd=(bazel)
+elif command -v bazelisk >/dev/null 2>&1; then
+  bazel_cmd=(bazelisk)
+else
+  echo "bazel or bazelisk is required on PATH" >&2
   exit 1
 fi
 
@@ -199,12 +203,12 @@ chmod +x "${bazel_dir}/node_modules_smoke_test.sh"
 
 (
   cd "${bazel_dir}"
-  bazel build @node_modules//:node_modules >/dev/null
-  bazel test //:node_modules_smoke_test >/dev/null
-  bazel run //:web_build -- --emptyOutDir >/dev/null
+  "${bazel_cmd[@]}" build @node_modules//:node_modules >/dev/null
+  "${bazel_cmd[@]}" test //:node_modules_smoke_test >/dev/null
+  "${bazel_cmd[@]}" run //:web_build -- --emptyOutDir >/dev/null
 )
 
-output_base="$(cd "${bazel_dir}" && bazel info output_base)"
+output_base="$(cd "${bazel_dir}" && "${bazel_cmd[@]}" info output_base)"
 bazel_repo_dir="$(find "${output_base}/external" -maxdepth 1 -type d -name '*+node_modules' | head -n 1)"
 
 if [[ -z ${bazel_repo_dir} ]]; then
@@ -238,6 +242,8 @@ root = sys.argv[1]
 
 def include(rel):
     if rel == "node_modules" or rel.startswith("node_modules/"):
+        if rel == "node_modules/.rules_bun" or rel.startswith("node_modules/.rules_bun/"):
+            return False
         return True
     if rel.startswith("packages/") and "/node_modules" in rel:
         return True
@@ -283,6 +289,8 @@ root = sys.argv[1]
 
 def include(rel):
     if rel == "node_modules" or rel.startswith("node_modules/"):
+        if rel == "node_modules/.rules_bun" or rel.startswith("node_modules/.rules_bun/"):
+            return False
         return True
     if rel.startswith("packages/") and "/node_modules" in rel:
         return True
@@ -343,6 +351,8 @@ for dirpath, dirnames, filenames in os.walk(root, topdown=True, followlinks=Fals
     for name in dirnames + filenames:
         full = os.path.join(dirpath, name)
         rel = os.path.join(rel_dir, name) if rel_dir else name
+        if rel == ".rules_bun" or rel.startswith(".rules_bun/"):
+            continue
         st = os.lstat(full)
         mode = st.st_mode
         if stat.S_ISLNK(mode):
@@ -379,6 +389,8 @@ for dirpath, dirnames, filenames in os.walk(root, topdown=True, followlinks=Fals
     for name in dirnames + filenames:
         full = os.path.join(dirpath, name)
         rel = os.path.join(rel_dir, name) if rel_dir else name
+        if rel == ".rules_bun" or rel.startswith(".rules_bun/"):
+            continue
         st = os.lstat(full)
         mode = st.st_mode
         if stat.S_ISLNK(mode):
@@ -411,7 +423,7 @@ rm -rf "${plain_dist_dir}" "${bazel_dist_dir}"
 
 (
   cd "${bazel_dir}"
-  bazel run //:web_build -- --emptyOutDir --outDir "${bazel_dist_dir}" >/dev/null
+  "${bazel_cmd[@]}" run //:web_build -- --emptyOutDir --outDir "${bazel_dist_dir}" >/dev/null
 )
 
 if [[ ! -d ${plain_dist_dir} ]]; then
diff --git a/tests/integration_test/BUILD.bazel b/tests/integration_test/BUILD.bazel
index 672d7f0..902db4d 100644
--- a/tests/integration_test/BUILD.bazel
+++ b/tests/integration_test/BUILD.bazel
@@ -4,6 +4,7 @@ test_suite(
     name = "examples_test",
     tests = [
         ":examples_basic_run_e2e_test",
+        ":examples_basic_hot_restart_shape_test",
         ":examples_workspace_bundle_e2e_test",
         ":examples_workspace_catalog_shape_test",
         ":examples_vite_monorepo_catalog_shape_test",
@@ -31,6 +32,13 @@ sh_test(
     data = ["//examples/basic:web_dev"],
 )
 
+sh_test(
+    name = "examples_basic_hot_restart_shape_test",
+    srcs = ["examples_basic_hot_restart_shape_test.sh"],
+    args = ["$(location //examples/basic:web_dev_hot_restart)"],
+    data = ["//examples/basic:web_dev_hot_restart"],
+)
+
 sh_test(
     name = "examples_workspace_bundle_e2e_test",
     srcs = ["examples_workspace_bundle_e2e_test.sh"],
diff --git a/tests/integration_test/examples_basic_hot_restart_shape_test.sh b/tests/integration_test/examples_basic_hot_restart_shape_test.sh
new file mode 100755
index 0000000..9656f1c
--- /dev/null
+++ b/tests/integration_test/examples_basic_hot_restart_shape_test.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+binary="$1"
+
+grep -Fq -- 'watch_mode="hot"' "${binary}"
+grep -Fq -- 'bun_args+=("--hot")' "${binary}"
+grep -Fq -- '--no-clear-screen' "${binary}"
+grep -Fq -- 'if [[ 1 -eq 0 ]]; then' "${binary}"
+grep -Fq -- 'readarray -t restart_paths' "${binary}"
+grep -Fq -- 'examples/basic/README.md' "${binary}"
diff --git a/tests/js_compat_test/BUILD.bazel b/tests/js_compat_test/BUILD.bazel
index 5363bf7..79ff97c 100644
--- a/tests/js_compat_test/BUILD.bazel
+++ b/tests/js_compat_test/BUILD.bazel
@@ -39,3 +39,30 @@ sh_test(
     args = ["$(location :compat_devserver)"],
     data = [":compat_devserver"],
 )
+
+js_run_devserver(
+    name = "compat_devserver_with_package_json",
+    tool = ":compat_bin",
+    package_json = "app/package.json",
+    working_dir = "package",
+)
+
+js_run_devserver(
+    name = "compat_devserver_with_package_dir_hint",
+    tool = ":compat_bin",
+    package_dir_hint = "app",
+    working_dir = "package",
+)
+
+sh_test(
+    name = "js_run_devserver_workspace_shape_test",
+    srcs = ["verify_workspace_shape.sh"],
+    args = [
+        "$(location :compat_devserver_with_package_json)",
+        "$(location :compat_devserver_with_package_dir_hint)",
+    ],
+    data = [
+        ":compat_devserver_with_package_json",
+        ":compat_devserver_with_package_dir_hint",
+    ],
+)
diff --git a/tests/js_compat_test/app/package.json b/tests/js_compat_test/app/package.json
new file mode 100644
index 0000000..f46ec7c
--- /dev/null
+++ b/tests/js_compat_test/app/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "js-compat-app",
+  "private": true
+}
diff --git a/tests/js_compat_test/verify_workspace_shape.sh b/tests/js_compat_test/verify_workspace_shape.sh
new file mode 100755
index 0000000..b99865b
--- /dev/null
+++ b/tests/js_compat_test/verify_workspace_shape.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+package_json_launcher="$1"
+package_dir_hint_launcher="$2"
+
+grep -Fq -- 'package_json="${runfiles_dir}/_main/tests/js_compat_test/app/package.json"' "${package_json_launcher}"
+grep -Fq -- 'package_rel_dir_hint="."' "${package_json_launcher}"
+grep -Fq -- 'working_dir_mode="package"' "${package_json_launcher}"
+
+grep -Fq -- 'package_json=""' "${package_dir_hint_launcher}"
+grep -Fq -- 'package_rel_dir_hint="app"' "${package_dir_hint_launcher}"
+grep -Fq -- 'working_dir_mode="package"' "${package_dir_hint_launcher}"
diff --git a/tests/npm_compat_test/BUILD.bazel b/tests/npm_compat_test/BUILD.bazel
new file mode 100644
index 0000000..2ed0064
--- /dev/null
+++ b/tests/npm_compat_test/BUILD.bazel
@@ -0,0 +1,59 @@
+load("@rules_shell//shell:sh_test.bzl", "sh_test")
+
+config_setting(
+    name = "linux_x86_64",
+    constraint_values = [
+        "@platforms//os:linux",
+        "@platforms//cpu:x86_64",
+    ],
+)
+
+config_setting(
+    name = "linux_aarch64",
+    constraint_values = [
+        "@platforms//os:linux",
+        "@platforms//cpu:aarch64",
+    ],
+)
+
+config_setting(
+    name = "darwin_x86_64",
+    constraint_values = [
+        "@platforms//os:macos",
+        "@platforms//cpu:x86_64",
+    ],
+)
+
+config_setting(
+    name = "darwin_aarch64",
+    constraint_values = [
+        "@platforms//os:macos",
+        "@platforms//cpu:aarch64",
+    ],
+)
+
+sh_test(
+    name = "npm_translate_lock_workspace_test",
+    srcs = ["npm_translate_lock_workspace_test.sh"],
+    env_inherit = ["PATH"],
+    args = select({
+        ":linux_x86_64": ["$(location @bun_linux_x64//:bun)"],
+        ":linux_aarch64": ["$(location @bun_linux_aarch64//:bun)"],
+        ":darwin_x86_64": ["$(location @bun_darwin_x64//:bun)"],
+        ":darwin_aarch64": ["$(location @bun_darwin_aarch64//:bun)"],
+        "//conditions:default": ["$(location @bun_linux_x64//:bun)"],
+    }),
+    data = select({
+        ":linux_x86_64": ["@bun_linux_x64//:bun"],
+        ":linux_aarch64": ["@bun_linux_aarch64//:bun"],
+        ":darwin_x86_64": ["@bun_darwin_x64//:bun"],
+        ":darwin_aarch64": ["@bun_darwin_aarch64//:bun"],
+        "//conditions:default": ["@bun_linux_x64//:bun"],
+    }) + [
+        "//:repo_runtime_files",
+        "//bun:repo_runtime_files",
+        "//internal:repo_runtime_files",
+        "//js:repo_runtime_files",
+        "//npm:repo_runtime_files",
+    ],
+)
diff --git a/tests/npm_compat_test/npm_translate_lock_workspace_test.sh b/tests/npm_compat_test/npm_translate_lock_workspace_test.sh
index fed8313..41ad8ca 100755
--- a/tests/npm_compat_test/npm_translate_lock_workspace_test.sh
+++ b/tests/npm_compat_test/npm_translate_lock_workspace_test.sh
@@ -1,15 +1,17 @@
 #!/usr/bin/env bash
 set -euo pipefail
 
-nix_cmd="${NIX:-/nix/var/nix/profiles/default/bin/nix}"
-if [[ ! -x ${nix_cmd} ]]; then
-  nix_cmd="$(command -v nix || true)"
-fi
-if [[ -z ${nix_cmd} || ! -x ${nix_cmd} ]]; then
-  echo "nix is required to launch bazel from the repo dev shell" >&2
+if command -v bazel >/dev/null 2>&1; then
+  bazel_bin="$(command -v bazel)"
+elif command -v bazelisk >/dev/null 2>&1; then
+  bazel_bin="$(command -v bazelisk)"
+else
+  echo "bazel or bazelisk is required on PATH" >&2
   exit 1
 fi
 
+bun_path="${1:-bun}"
+
 script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
 rules_bun_root="$(cd "${script_dir}/../.." && pwd -P)"
 
@@ -35,10 +37,7 @@ import isNumber from "is-number";
 console.log(`compat:${isNumber(42)}`);
 JS
 
-(
-  cd "${rules_bun_root}" &&
-    "${nix_cmd}" develop -c bash -lc 'bun install --cwd "$1" >/dev/null' bash "${fixture_dir}"
-)
+"${bun_path}" install --cwd "${fixture_dir}" >/dev/null
 rm -rf "${fixture_dir}/node_modules"
 
 cat >"${fixture_dir}/MODULE.bazel" <&2
diff --git a/tests/script_test/BUILD.bazel b/tests/script_test/BUILD.bazel
index 9336e2e..e9e89e8 100644
--- a/tests/script_test/BUILD.bazel
+++ b/tests/script_test/BUILD.bazel
@@ -165,3 +165,54 @@ sh_test(
     args = ["$(location :workspace_filtered_script)"],
     data = [":workspace_filtered_script"],
 )
+
+bun_script(
+    name = "workspace_parallel_script",
+    script = "say",
+    package_json = "workspace_run/package.json",
+    data = [
+        "workspace_run/packages/pkg-a/package.json",
+        "workspace_run/packages/pkg-a/say.ts",
+        "workspace_run/packages/pkg-b/package.json",
+        "workspace_run/packages/pkg-b/say.ts",
+    ],
+    workspaces = True,
+    execution_mode = "parallel",
+)
+
+sh_test(
+    name = "bun_script_workspace_parallel_test",
+    srcs = ["run_workspace_parallel.sh"],
+    args = ["$(location :workspace_parallel_script)"],
+    data = [":workspace_parallel_script"],
+)
+
+bun_script(
+    name = "workspace_flagged_script",
+    script = "say",
+    package_json = "workspace_run/package.json",
+    data = [
+        "workspace_run/packages/pkg-a/package.json",
+        "workspace_run/packages/pkg-a/say.ts",
+        "workspace_run/packages/pkg-b/package.json",
+        "workspace_run/packages/pkg-b/say.ts",
+    ],
+    workspaces = True,
+    execution_mode = "parallel",
+    no_exit_on_error = True,
+    shell = "system",
+)
+
+sh_test(
+    name = "bun_script_workspace_flag_shape_test",
+    srcs = ["verify_launcher_flags.sh"],
+    args = [
+        "$(location :workspace_flagged_script)",
+        "--workspaces",
+        "--parallel",
+        "--no-exit-on-error",
+        "--shell",
+        "system",
+    ],
+    data = [":workspace_flagged_script"],
+)
diff --git a/tests/script_test/run_workspace_parallel.sh b/tests/script_test/run_workspace_parallel.sh
new file mode 100755
index 0000000..b59dd92
--- /dev/null
+++ b/tests/script_test/run_workspace_parallel.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+script_bin="$1"
+output="$(${script_bin})"
+
+if [[ ${output} != *"pkg-a"* ]]; then
+  echo "Expected workspace parallel run output to include pkg-a: ${output}" >&2
+  exit 1
+fi
+
+if [[ ${output} != *"pkg-b"* ]]; then
+  echo "Expected workspace parallel run output to include pkg-b: ${output}" >&2
+  exit 1
+fi
diff --git a/tests/script_test/verify_launcher_flags.sh b/tests/script_test/verify_launcher_flags.sh
new file mode 100755
index 0000000..515cd41
--- /dev/null
+++ b/tests/script_test/verify_launcher_flags.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+binary="$1"
+shift
+
+for expected in "$@"; do
+  if ! grep -Fq -- "${expected}" "${binary}"; then
+    echo "Expected ${binary} to contain ${expected}" >&2
+    exit 1
+  fi
+done