feat: improve rules_js parity

This commit is contained in:
eric
2026-03-14 23:50:26 +01:00
parent d7a6d6b0ba
commit c446f23a35
36 changed files with 1683 additions and 639 deletions

649
internal/workspace.bzl Normal file
View File

@@ -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,
)