diff --git a/MODULE.bazel b/MODULE.bazel
index 521cdb4..3eeae10 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -26,6 +26,10 @@ rules_ts_ext.deps(
use_repo(rules_ts_ext, "npm_typescript")
+# Prebuilt host hermesc, referenced by the hermes_compile rule default.
+hermes_ext = use_extension("//javascript:extensions.bzl", "hermes")
+use_repo(hermes_ext, "rn_hermesc")
+
bazel_dep(name = "rules_kotlin", version = "2.1.8")
bazel_dep(name = "rules_java", version = "7.2.0")
bazel_dep(name = "rules_jvm_external", version = "6.8")
diff --git a/docs/javascript.md b/docs/javascript.md
index 292af8e..d278771 100644
--- a/docs/javascript.md
+++ b/docs/javascript.md
@@ -67,9 +67,9 @@ A test target that runs eslint on the given sources.
load("@rules_player//javascript:defs.bzl", "js_pipeline")
js_pipeline(package_name, name, srcs, package_json, root_package_json, vitest_config, tsup_config,
- tsconfig, node_modules, deps, native_bundle, private, peer_deps, create_package_json_args,
- include_packaging_targets, test_deps, lint_deps, build_deps, benchmark_envs,
- _extra_replace_prefixes)
+ tsconfig, node_modules, deps, native_bundle, emit_hbc, private, peer_deps,
+ create_package_json_args, include_packaging_targets, test_deps, lint_deps, build_deps,
+ benchmark_envs, _extra_replace_prefixes)
The main entry point for any JS/TS project. `js_pipeline` should be the only thing you need in your BUILD file.
@@ -93,6 +93,7 @@ Creates a js_library, npm_package, and test targets for a given package.
| node_modules | The base node_modules to pull dependencies from (defaults to //:node_modules). | `"//:node_modules"` |
| deps | The dependencies for the package. | `[]` |
| native_bundle | The name for the native bundle global if defined. | `None` |
+| emit_hbc | When True (with native_bundle set), also compile the native bundle to Hermes bytecode, exposed as `:hbc`. Used for cross-platform Android plugins. | `False` |
| private | Whether or not the package should be private (skipping an npm release). | `False` |
| peer_deps | The peer dependencies for the package. | `[]` |
| create_package_json_args | Additional arguments to pass to the package_json creation | `{}` |
diff --git a/javascript/extensions.bzl b/javascript/extensions.bzl
new file mode 100644
index 0000000..86e4af5
--- /dev/null
+++ b/javascript/extensions.bzl
@@ -0,0 +1,21 @@
+"""Module extensions for JavaScript rules."""
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+# Pinned react-native version whose npm tarball ships the prebuilt host hermesc.
+_RN_VERSION = "0.78.1"
+_RN_TARBALL_SHA256 = "bf4198ba712c09df3de6d493cfaf968498cddb7107ecd48a081a1ce32d18be06"
+
+def _hermes_impl(_module_ctx):
+ http_archive(
+ name = "rn_hermesc",
+ build_file = "@rules_player//javascript/private:hermesc.BUILD",
+ sha256 = _RN_TARBALL_SHA256,
+ strip_prefix = "package",
+ url = "https://registry.npmjs.org/react-native/-/react-native-{v}.tgz".format(v = _RN_VERSION),
+ )
+
+hermes = module_extension(
+ implementation = _hermes_impl,
+ doc = "Provides `@rn_hermesc//:hermesc`, the prebuilt host Hermes compiler.",
+)
diff --git a/javascript/private/BUILD b/javascript/private/BUILD
index 81396de..0982764 100644
--- a/javascript/private/BUILD
+++ b/javascript/private/BUILD
@@ -37,6 +37,7 @@ bzl_library(
],
deps = [
":eslint",
+ ":hermesc",
":package_json",
":tsup",
":utils",
@@ -48,6 +49,15 @@ bzl_library(
],
)
+bzl_library(
+ name = "hermesc",
+ srcs = ["hermesc.bzl"],
+ visibility = [
+ "//docs:__subpackages__",
+ "//javascript:__subpackages__",
+ ],
+)
+
bzl_library(
name = "vitest",
srcs = ["vitest.bzl"],
diff --git a/javascript/private/hermesc.BUILD b/javascript/private/hermesc.BUILD
new file mode 100644
index 0000000..64f2d73
--- /dev/null
+++ b/javascript/private/hermesc.BUILD
@@ -0,0 +1,18 @@
+# Prebuilt Hermes compiler (hermesc) shipped in the react-native npm tarball.
+# Host-only: compiles JS -> HBC on the build machine. Selected by host OS.
+load("@bazel_skylib//rules:native_binary.bzl", "native_binary")
+
+native_binary(
+ name = "hermesc",
+ src = select({
+ "@bazel_tools//src/conditions:darwin": "sdks/hermesc/osx-bin/hermesc",
+ "@bazel_tools//src/conditions:windows": "sdks/hermesc/win64-bin/hermesc.exe",
+ "//conditions:default": "sdks/hermesc/linux64-bin/hermesc",
+ }),
+ data = select({
+ "@bazel_tools//src/conditions:windows": glob(["sdks/hermesc/win64-bin/*.dll"]),
+ "//conditions:default": [],
+ }),
+ out = "hermesc",
+ visibility = ["//visibility:public"],
+)
diff --git a/javascript/private/hermesc.bzl b/javascript/private/hermesc.bzl
new file mode 100644
index 0000000..c0eb75d
--- /dev/null
+++ b/javascript/private/hermesc.bzl
@@ -0,0 +1,69 @@
+"""
+Hermes bytecode (HBC) compilation.
+
+`hermes_compile` runs the prebuilt host `hermesc` over a single JS file to emit
+Hermes bytecode. `hermes_bundle` is a convenience macro that extracts the
+`.native.js` produced by a `js_pipeline` native bundle and compiles it.
+"""
+
+def _hermes_compile_impl(context):
+ input = context.file.js
+ hbc = context.actions.declare_file("%s.hbc" % input.basename)
+
+ args = context.actions.args()
+ args.add("-emit-binary")
+ args.add("-out", hbc)
+ args.add(input)
+
+ context.actions.run(
+ mnemonic = "HermesC",
+ executable = context.executable._hermesc,
+ arguments = [args],
+ inputs = depset([input]),
+ outputs = [hbc],
+ )
+
+ return [DefaultInfo(files = depset([hbc]))]
+
+hermes_compile = rule(
+ implementation = _hermes_compile_impl,
+ attrs = {
+ "js": attr.label(
+ allow_single_file = True,
+ doc = "Single JS file to compile to HBC.",
+ ),
+ "_hermesc": attr.label(
+ default = Label("@rn_hermesc//:hermesc"),
+ allow_single_file = True,
+ executable = True,
+ cfg = "exec",
+ ),
+ },
+)
+
+def hermes_bundle(name, native_bundle, bundle_name, visibility = None):
+ """Compile a js_pipeline native bundle to Hermes bytecode.
+
+ Args:
+ name: Name of the resulting HBC target.
+ native_bundle: The `:_native_bundle` target from js_pipeline.
+ bundle_name: The `native_bundle` string passed to js_pipeline (e.g. "Player").
+ visibility: Visibility for the generated targets.
+ """
+ native_js = "%s.native.js" % bundle_name
+ extract_name = "%s_native_js" % name
+
+ # js_run_binary emits a set of files (dist/.native.js + .map);
+ # hermesc needs a single file, so pull it out flat.
+ native.genrule(
+ name = extract_name,
+ srcs = [native_bundle],
+ outs = [native_js],
+ cmd = "echo $(SRCS) | tr ' ' '\\n' | grep %s$$ | xargs -I {} cp {} $(OUTS)" % native_js,
+ )
+
+ hermes_compile(
+ name = name,
+ js = ":" + extract_name,
+ visibility = visibility,
+ )
diff --git a/javascript/private/js_pipeline.bzl b/javascript/private/js_pipeline.bzl
index 5e02023..a9da503 100644
--- a/javascript/private/js_pipeline.bzl
+++ b/javascript/private/js_pipeline.bzl
@@ -7,6 +7,7 @@ load("@aspect_rules_js//npm:defs.bzl", "npm_package")
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
load("@bazel_skylib//rules:expand_template.bzl", "expand_template")
load(":eslint.bzl", "eslint_test")
+load(":hermesc.bzl", "hermes_bundle")
load(":package_json.bzl", "create_package_json")
load(":tsup.bzl", "tsup_build", "tsup_native_build")
load(":utils.bzl", "filter_empty", "without_tests")
@@ -29,6 +30,7 @@ def js_pipeline(
node_modules = "//:node_modules",
deps = [],
native_bundle = None,
+ emit_hbc = False,
private = False,
peer_deps = [],
create_package_json_args = {},
@@ -55,6 +57,8 @@ def js_pipeline(
node_modules: The base node_modules to pull dependencies from (defaults to //:node_modules).
deps: The dependencies for the package.
native_bundle: The name for the native bundle global if defined.
+ emit_hbc: When True (with native_bundle set), also compile the native bundle to
+ Hermes bytecode, exposed as `:hbc`. Used for cross-platform Android plugins.
private: Whether or not the package should be private (skipping an npm release).
create_package_json_args: Additional arguments to pass to the package_json creation
include_packaging_targets: Additional dependencies to add to the package target
@@ -105,6 +109,14 @@ def js_pipeline(
node_modules = node_modules,
)
+ if emit_hbc:
+ hermes_bundle(
+ name = name + "_hbc",
+ native_bundle = native_bundle_target,
+ bundle_name = native_bundle,
+ visibility = ["//visibility:public"],
+ )
+
if tsconfig == None:
tsconfig_name = "{}_tsconfig".format(name)
prefix = "../" * len(native.package_name().split("/"))