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

View File

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