diff --git a/MODULE.bazel b/MODULE.bazel index 253e1b4c8b..8bc1747b1b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -18,6 +18,9 @@ bazel_dep(name = "bazel_skylib", version = "1.5.0") bazel_dep(name = "platforms", version = "1.0.0") bazel_dep(name = "rules_nodejs", version = "6.7.3") +# NB: LOWER BOUND on earliest BCR release of protobuf module, to avoid upgrading the root module by accident +bazel_dep(name = "protobuf", version = "3.19.6") + # Ensure any version of aspect_bazel_lib used includes: # https://github.com/bazel-contrib/bazel-lib/commit/371362199e5e5bde58aeb15b54ea6cfb164af337 # https://github.com/bazel-contrib/bazel-lib/commit/ceaff54210291292f35d03bd4bbc0b6ceb40e77a diff --git a/js/BUILD.bazel b/js/BUILD.bazel index f9d0335493..7c7c8aa59a 100644 --- a/js/BUILD.bazel +++ b/js/BUILD.bazel @@ -2,11 +2,6 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") -exports_files( - glob(["*.bzl"]), - visibility = ["//visibility:public"], -) - bzl_library( name = "defs", srcs = ["defs.bzl"], @@ -27,6 +22,15 @@ starlark_doc_extract( deps = [":defs"], ) +bzl_library( + name = "proto", + srcs = ["proto.bzl"], + visibility = ["//visibility:public"], + deps = [ + "//js/private:proto", + ], +) + bzl_library( name = "libs", srcs = ["libs.bzl"], diff --git a/js/private/BUILD.bazel b/js/private/BUILD.bazel index e5130e901f..d67d36401e 100644 --- a/js/private/BUILD.bazel +++ b/js/private/BUILD.bazel @@ -4,8 +4,6 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") package(default_visibility = ["//visibility:public"]) -exports_files(glob(["*.bzl"])) - exports_files([ "js_binary.sh.tpl", "node_wrapper.bat", @@ -24,6 +22,21 @@ bzl_library( ], ) +bzl_library( + name = "proto", + srcs = [ + "proto.bzl", + "proto_common.bzl", + ], + visibility = ["//visibility:public"], + deps = [ + ":js_helpers", + "//js:providers", + "@protobuf//bazel/common:proto_common_bzl", + "@protobuf//bazel/common:proto_info_bzl", + ], +) + bzl_library( name = "js_binary", srcs = ["js_binary.bzl"], @@ -56,6 +69,7 @@ bzl_library( deps = [ ":js_helpers", ":js_info", + ":proto", "@bazel_lib//lib:copy_to_bin", "@bazel_skylib//lib:dicts", "@bazel_tools//tools/build_defs/repo:cache.bzl", diff --git a/js/private/js_library.bzl b/js/private/js_library.bzl index b7e8d445f8..968643e9e0 100644 --- a/js/private/js_library.bzl +++ b/js/private/js_library.bzl @@ -26,6 +26,7 @@ js_library( load("@bazel_lib//lib:copy_to_bin.bzl", "COPY_FILE_TO_BIN_TOOLCHAINS") load(":js_helpers.bzl", "copy_js_file_to_bin_action", "gather_runfiles") load(":js_info.bzl", "JsInfo", "js_info") +load(":proto.bzl", "js_proto_aspect") _DOC = """A library of JavaScript sources. Provides JsInfo, the primary provider used in rules_js and derivative rule sets. @@ -101,6 +102,7 @@ runtime dependency on this target. {linked_npm_deps} """.format(linked_npm_deps = _LINKED_NPM_DEPS_DOCSTRING), providers = [JsInfo], + aspects = [js_proto_aspect], ), "data": attr.label_list( doc = """Runtime dependencies to include in binaries/tests that depend on this target. diff --git a/js/private/proto.bzl b/js/private/proto.bzl new file mode 100644 index 0000000000..5f0e4d7f5f --- /dev/null +++ b/js/private/proto.bzl @@ -0,0 +1,92 @@ +"""An aspect that generates JavaScript and TypeScript code from .proto files. + +This Bazel integration follows the "Local Generation" mechanism described at +https://connectrpc.com/docs/web/generating-code#local-generation, +using packages such as `@bufbuild/protoc-gen-es` and `@connectrpc/protoc-gen-connect-query` +as plugins to protoc. + +The aspect converts a ProtoInfo provider into a JsInfo provider so that proto_library may be a dep to JS rules. +""" + +load("@protobuf//bazel/common:proto_info.bzl", "ProtoInfo") +load("//js:providers.bzl", "JsInfo", "js_info") +load( + "//js/private:js_helpers.bzl", + "gather_npm_package_store_infos", + "gather_npm_sources", + "gather_transitive_sources", + "gather_transitive_types", +) +load(":proto_common.bzl", "proto_common") + +LANG_PROTO_TOOLCHAIN = Label("//js/toolchains:protoc_plugin") +PROTOC_TOOLCHAIN = Label("@protobuf//bazel/private:proto_toolchain_type") + +def _js_proto_aspect_impl(target, ctx): + proto_info = target[ProtoInfo] + protoc_info = ctx.toolchains[PROTOC_TOOLCHAIN].proto + proto_lang_toolchain_info = ctx.toolchains[LANG_PROTO_TOOLCHAIN].proto + js_outputs = proto_common.declare_generated_files(ctx.actions, proto_info, "_pb.js") + dts_outputs = proto_common.declare_generated_files(ctx.actions, proto_info, "_pb.d.ts") + output_root = js_outputs[0].root + + args = ctx.actions.args() + args.add(proto_lang_toolchain_info.plugin.executable, format = proto_lang_toolchain_info.plugin_format_flag) + proto_outdir = proto_common.output_directory(proto_info, output_root) + args.add_all((proto_lang_toolchain_info.out_replacement_format_flag % proto_outdir).split(" ")) + args.add("--descriptor_set_in") + args.add_joined(proto_info.transitive_descriptor_sets, join_with = ctx.configuration.host_path_separator) + + # Vendored: https://github.com/protocolbuffers/protobuf/blob/v31.1/bazel/common/proto_common.bzl#L193-L204 + # Protoc searches for .protos -I paths in order they are given and then + # uses the path within the directory as the package. + # This requires ordering the paths from most specific (longest) to least + # specific ones, so that no path in the list is a prefix of any of the + # following paths in the list. + # For example: 'bazel-out/k8-fastbuild/bin/external/foo' needs to be listed + # before 'bazel-out/k8-fastbuild/bin'. If not, protoc will discover file under + # the shorter path and use 'external/foo/...' as its package path. + args.add_all(proto_info.transitive_proto_path, map_each = proto_common.import_virtual_proto_path) + args.add_all(proto_info.transitive_proto_path, map_each = proto_common.import_repo_proto_path) + args.add_all(proto_info.transitive_proto_path, map_each = proto_common.import_main_output_proto_path) + args.add("-I.") # Needs to come last + + args.add_all(proto_info.direct_sources) + + ctx.actions.run( + executable = protoc_info.proto_compiler.executable, + arguments = [args], + progress_message = "Generating .js/.d.ts from %{label}", + mnemonic = "JsProtocGenerate", + env = {"BAZEL_BINDIR": output_root.path}, + tools = [proto_lang_toolchain_info.plugin, protoc_info.proto_compiler], + inputs = depset(proto_info.direct_sources, transitive = [proto_info.transitive_descriptor_sets]), + outputs = js_outputs + dts_outputs, + use_default_shell_env = True, + ) + + return [ + js_info( + target = ctx.label, + sources = depset(js_outputs), + types = depset(dts_outputs), + transitive_sources = gather_transitive_sources(js_outputs, ctx.rule.attr.deps), + transitive_types = gather_transitive_types(dts_outputs, ctx.rule.attr.deps), + npm_sources = gather_npm_sources(srcs = [], deps = [proto_lang_toolchain_info.runtime]), + npm_package_store_infos = gather_npm_package_store_infos([proto_lang_toolchain_info.runtime]), + ), + ] + +js_proto_aspect = aspect( + implementation = _js_proto_aspect_impl, + # Traverse the "deps" graph edges starting from the target + attr_aspects = ["deps"], + # Only visit nodes that produce a ProtoInfo provider + required_providers = [ProtoInfo], + # Be a valid dependency of a ts_project rule + provides = [JsInfo], + toolchains = [ + LANG_PROTO_TOOLCHAIN, + PROTOC_TOOLCHAIN, + ], +) diff --git a/js/private/proto_common.bzl b/js/private/proto_common.bzl new file mode 100644 index 0000000000..012dff57c9 --- /dev/null +++ b/js/private/proto_common.bzl @@ -0,0 +1,58 @@ +"""Fork of +https://github.com/protocolbuffers/protobuf/blob/95dc8a02ad7d64527f0850c17edb1b86d3996ad8/bazel/common/proto_common.bzl#L15 +that exposes some helper functions that _compile uses, so we can implement our own version of it. +""" + +load("@protobuf//bazel/common:proto_common.bzl", _proto_common = "proto_common") + +def _import_virtual_proto_path(path): + """Imports all paths for virtual imports. + + They're of the form: + 'bazel-out/k8-fastbuild/bin/external/foo/e/_virtual_imports/e' or + 'bazel-out/foo/k8-fastbuild/bin/e/_virtual_imports/e'""" + if path.count("/") > 4: + return "-I%s" % path + return None + +def _import_repo_proto_path(path): + """Imports all paths for generated files in external repositories. + + They are of the form: + 'bazel-out/k8-fastbuild/bin/external/foo' or + 'bazel-out/foo/k8-fastbuild/bin'""" + path_count = path.count("/") + if path_count > 2 and path_count <= 4: + return "-I%s" % path + return None + +def _import_main_output_proto_path(path): + """Imports all paths for generated files or source files in external repositories. + + They're of the form: + 'bazel-out/k8-fastbuild/bin' + 'external/foo' + '../foo' + """ + if path.count("/") <= 2 and path != ".": + return "-I%s" % path + return None + +def _output_directory(proto_info, root): + proto_source_root = proto_info.proto_source_root + if proto_source_root.startswith(root.path): + #TODO: remove this branch when bin_dir is removed from proto_source_root + proto_source_root = proto_source_root.removeprefix(root.path).removeprefix("/") + + if proto_source_root == "" or proto_source_root == ".": + return root.path + + return root.path + "/" + proto_source_root + +proto_common = struct( + declare_generated_files = _proto_common.declare_generated_files, + import_virtual_proto_path = _import_virtual_proto_path, + import_repo_proto_path = _import_repo_proto_path, + import_main_output_proto_path = _import_main_output_proto_path, + output_directory = _output_directory, +) diff --git a/js/proto.bzl b/js/proto.bzl new file mode 100644 index 0000000000..f0af9d982e --- /dev/null +++ b/js/proto.bzl @@ -0,0 +1,16 @@ +"""Protobuf and gRPC support for JavaScript and TypeScript. + +See +- https://connectrpc.com/docs/web/getting-started +- https://connectrpc.com/docs/node/getting-started +""" + +load("@protobuf//bazel/toolchains:proto_lang_toolchain.bzl", "proto_lang_toolchain") +load("//js/private:proto.bzl", "LANG_PROTO_TOOLCHAIN") + +def js_proto_toolchain(name, **kwargs): + proto_lang_toolchain( + name = name, + toolchain_type = LANG_PROTO_TOOLCHAIN, + **kwargs + ) diff --git a/js/toolchains/BUILD b/js/toolchains/BUILD new file mode 100644 index 0000000000..de75cffe14 --- /dev/null +++ b/js/toolchains/BUILD @@ -0,0 +1,3 @@ +package(default_visibility = ["//visibility:public"]) + +toolchain_type(name = "protoc_plugin")