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("/"))