From 5210cdb3d70b5964f66e02874df36b601dc0b5a3 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Tue, 10 Feb 2026 18:26:37 -0800 Subject: [PATCH 1/6] feat: add js_proto_toolchain Design: - user expected to register a toolchain providing their protoc plugin for gen-es - Adapt ProtoInfo to JsInfo so a proto_library target can appear anywhere a js_library does Example of use currently in https://github.com/aspect-build/rules_ts/pull/894 --- MODULE.bazel | 3 + js/private/js_library.bzl | 2 + js/private/proto.bzl | 187 ++++++++++++++++++++++++++++++++++++++ js/proto.bzl | 70 ++++++++++++++ js/toolchains/BUILD | 3 + 5 files changed, 265 insertions(+) create mode 100644 js/private/proto.bzl create mode 100644 js/proto.bzl create mode 100644 js/toolchains/BUILD 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/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..482e2bc298 --- /dev/null +++ b/js/private/proto.bzl @@ -0,0 +1,187 @@ +"""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", +) + +GEN_ES_PLUGIN_TOOLCHAIN = Label("//js/toolchains:protoc_plugin") +PROTOC_TOOLCHAIN = "@protobuf//bazel/private:proto_toolchain_type" +GEN_ES_OUTPUT_FILE = "{proto_file_basename}_pb.js" +GEN_ES_TYPINGS_OUTPUT_FILE = "{proto_file_basename}_pb.d.ts" + +# FIXME: support stripping import prefixes +def _js_proto_aspect_impl(target, ctx): + if not ProtoInfo in target: + # Should not be possible, since our aspect declaration has required_providers = [ProtoInfo] + fail("ts_proto_aspect_impl: target %s does not provide ProtoInfo" % target.label) + + # Skip generation for well-known-types (is this the right way??) + if target.label.workspace_root.startswith("external/protobuf"): + return js_info(target = target.label) + + protoc_info = ctx.toolchains[PROTOC_TOOLCHAIN].proto + gen_es_info = ctx.toolchains[GEN_ES_PLUGIN_TOOLCHAIN].proto + deps = [gen_es_info.runtime] + js_outputs = [] + dts_outputs = [] + + for src in target[ProtoInfo].direct_sources: + proto_file_basename = src.basename.replace(".proto", "") + js_output = ctx.actions.declare_file(GEN_ES_OUTPUT_FILE.format(proto_file_basename = proto_file_basename)) + dts_output = ctx.actions.declare_file(GEN_ES_TYPINGS_OUTPUT_FILE.format(proto_file_basename = proto_file_basename)) + + # Follow https://www.npmjs.com/package/@bufbuild/protoc-gen-es + # Create an action like + # bazel-out/k8-opt-exec-2B5CBBC6/bin/external/com_google_protobuf/protoc $@' '' \ + # '--plugin=protoc-gen-es=bazel-out/k8-opt-exec-2B5CBBC6/bin/plugin/bufbuild/protoc-gen-es.sh' \ + # '--es_opt=keep_empty_files=true' '--es_opt=target=ts' \ + # '--es_out=bazel-out/k8-fastbuild/bin' \ + # '--descriptor_set_in=bazel-out/k8-fastbuild/bin/external/com_google_protobuf/timestamp_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/thing/thing_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/place/place_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/person/person_proto-descriptor-set.proto.bin' \ + # example/person/person.proto + args = ctx.actions.args() + args.add_joined(["--plugin", "protoc-gen-es", gen_es_info.plugin.executable], join_with = "=") + args.add_joined(["--es_out", ctx.bin_dir.path], join_with = "=") + args.add_all(gen_es_info.out_replacement_format_flag.split(" ")) + args.add("--descriptor_set_in") + args.add_joined(target[ProtoInfo].transitive_descriptor_sets, join_with = ctx.configuration.host_path_separator) + args.add_all(target[ProtoInfo].direct_sources) + ctx.actions.run( + executable = protoc_info.proto_compiler.executable, + arguments = [args], + progress_message = "Generating .js/.d.ts from %{label}", + mnemonic = "ProtocGenEs", + env = {"BAZEL_BINDIR": ctx.bin_dir.path}, + tools = [gen_es_info.plugin, protoc_info.proto_compiler], + inputs = depset(target[ProtoInfo].direct_sources, transitive = [target[ProtoInfo].transitive_descriptor_sets]), + outputs = [js_output, dts_output], + use_default_shell_env = True, + ) + js_outputs.append(js_output) + dts_outputs.append(dts_output) + + 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 = deps), + npm_package_store_infos = gather_npm_package_store_infos(deps), + ), + ] + +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 = [ + GEN_ES_PLUGIN_TOOLCHAIN, + PROTOC_TOOLCHAIN, + ], +) + +# def _windows_path_normalize(path): +# """Changes forward slashs to backslashs for Windows paths.""" +# host_is_windows = platform_utils.host_platform_is_windows() +# if host_is_windows: +# return path.replace("/", "\\") +# return path + +# # Vendored: https://github.com/protocolbuffers/protobuf/blob/v31.1/bazel/common/proto_common.bzl#L15-L23 +# 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 + +# # Vendored: https://github.com/protocolbuffers/protobuf/blob/v31.1/bazel/common/proto_common.bzl#L25-L34 +# 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 + +# # Vendored: https://github.com/protocolbuffers/protobuf/blob/v31.1/bazel/common/proto_common.bzl#L36-L46 +# 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 + +# # buildifier: disable=function-docstring-header +# def _protoc_action(ctx, proto_info, outputs): +# + +# # ensure that bin_dir doesn't get duplicated in the path +# # e.g. by proto_library(strip_import_prefix=...) +# proto_root = proto_info.proto_source_root +# if proto_root.startswith(ctx.bin_dir.path): +# proto_root = proto_root[len(ctx.bin_dir.path) + 1:] +# plugin_output = ctx.bin_dir.path + "/" + proto_root + +# # 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 = _import_virtual_proto_path) +# args.add_all(proto_info.transitive_proto_path, map_each = _import_repo_proto_path) +# args.add_all(proto_info.transitive_proto_path, map_each = _import_main_output_proto_path) +# args.add("-I.") # Needs to come last + +# def _declare_outs(ctx, info, ext): +# outs = proto_common.declare_generated_files(ctx.actions, info, "_pb" + ext) +# if ctx.attr.gen_connect_es: +# outs.extend(proto_common.declare_generated_files(ctx.actions, info, "_connect" + ext)) +# if ctx.attr.gen_connect_query: +# proto_sources = info.direct_sources +# proto_source_map = {src.basename: src for src in proto_sources} + +# # FIXME: we should refer to source files via labels instead of filenames +# for proto, services in ctx.attr.gen_connect_query_service_mapping.items(): +# if not proto in proto_source_map: +# fail("{} is not provided by proto_srcs".format(proto)) +# src = proto_source_map.get(proto) +# prefix = proto.replace(".proto", "") +# for service in services: +# outs.append(ctx.actions.declare_file("{}-{}_connectquery{}".format(prefix, service, ext), sibling = src)) + +# return outs diff --git a/js/proto.bzl b/js/proto.bzl new file mode 100644 index 0000000000..67efd8ce3b --- /dev/null +++ b/js/proto.bzl @@ -0,0 +1,70 @@ +"""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", "GEN_ES_PLUGIN_TOOLCHAIN") + +def js_proto_toolchain(name, protoc_plugin, runtime, command_line = ""): + """Declare a toolchain for the TypeScript gRPC protoc plugin. + + NB: the toolchain produced by this macro is actually named [name]_toolchain, so THAT is what you must register. + Even better, make a dedicated 'toolchains' directory and put all your toolchains in there, then register them all with 'register_toolchains("//path/to/toolchains:all")'. + name: any distinct target name, not used + plugin: the protoc plugin to use, typically @bufbuild/protoc-gen-es: + + load("@npm//tools:@bufbuild/protoc-gen-es/package_json.bzl", gen_es = "bin") + gen_es.protoc_gen_es_binary( + name = "protoc_gen_es", + ) + + runtime: dependency that the generated stub code takes, e.g. "//:node_modules/@bufbuild/protobuf" + command_line: command line arguments to pass to the protoc compiler, like "--es_opt=keep_empty_files=true --es_opt=target=js+dts --es_opt=import_extension=js" + """ + proto_lang_toolchain( + name = name, + plugin = protoc_plugin, + toolchain_type = GEN_ES_PLUGIN_TOOLCHAIN, + command_line = command_line, + runtime = runtime, + ) + +# FIXME: still want to have .d.ts files placed in the source tree, by diff.bzl +# if not copy_files: +# return +# if not files_to_copy: +# if not proto_srcs: +# fail("Either proto_srcs should be set, or copy_files should be False") +# files_to_copy = [s.replace(".proto", "_pb.d.ts") for s in proto_srcs] +# if gen_connect_es: +# files_to_copy.extend([s.replace(".proto", "_connect.d.ts") for s in proto_srcs]) +# if gen_connect_query: +# for proto, services in gen_connect_query_service_mapping.items(): +# files_to_copy.extend([proto.replace(".proto", "-{}_connectquery.d.ts".format(s)) for s in services]) + +# files_target = "_{}.filegroup".format(name) +# dir_target = "_{}.directory".format(name) +# copy_target = "{}.copy".format(name) + +# native.filegroup( +# name = files_target, +# srcs = [name], +# output_group = "types", +# ) + +# copy_to_directory( +# name = dir_target, +# srcs = [files_target], +# root_paths = ["**"], +# ) + +# write_source_files( +# name = copy_target, +# files = { +# f: make_directory_path("_{}_dirpath".format(f), dir_target, f) +# for f in files_to_copy +# }, +# ) 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") From eca2c6c27fae82f7d9db2cfe79140f0bf95d40fb Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Tue, 10 Feb 2026 18:47:20 -0800 Subject: [PATCH 2/6] add missing bzl_library --- js/BUILD.bazel | 14 +++++++++----- js/private/BUILD.bazel | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) 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..8fae2f160b 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,17 @@ bzl_library( ], ) +bzl_library( + name = "proto", + srcs = ["proto.bzl"], + visibility = ["//visibility:public"], + deps = [ + ":js_helpers", + "//js:providers", + "@protobuf//bazel/common:proto_info_bzl", + ], +) + bzl_library( name = "js_binary", srcs = ["js_binary.bzl"], @@ -56,6 +65,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", From e06f53215e91238329dac528ac3db426606325df Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Wed, 11 Feb 2026 08:31:42 -0800 Subject: [PATCH 3/6] code review comments --- js/private/proto.bzl | 161 +++++++++++-------------------------------- js/proto.bzl | 37 ---------- 2 files changed, 39 insertions(+), 159 deletions(-) diff --git a/js/private/proto.bzl b/js/private/proto.bzl index 482e2bc298..c0a18b48f9 100644 --- a/js/private/proto.bzl +++ b/js/private/proto.bzl @@ -19,19 +19,15 @@ load( ) GEN_ES_PLUGIN_TOOLCHAIN = Label("//js/toolchains:protoc_plugin") -PROTOC_TOOLCHAIN = "@protobuf//bazel/private:proto_toolchain_type" +PROTOC_TOOLCHAIN = Label("@protobuf//bazel/private:proto_toolchain_type") GEN_ES_OUTPUT_FILE = "{proto_file_basename}_pb.js" GEN_ES_TYPINGS_OUTPUT_FILE = "{proto_file_basename}_pb.d.ts" # FIXME: support stripping import prefixes def _js_proto_aspect_impl(target, ctx): - if not ProtoInfo in target: - # Should not be possible, since our aspect declaration has required_providers = [ProtoInfo] - fail("ts_proto_aspect_impl: target %s does not provide ProtoInfo" % target.label) - # Skip generation for well-known-types (is this the right way??) - if target.label.workspace_root.startswith("external/protobuf"): - return js_info(target = target.label) + # if target.label.workspace_root.startswith("external/protobuf"): + # return js_info(target = target.label) protoc_info = ctx.toolchains[PROTOC_TOOLCHAIN].proto gen_es_info = ctx.toolchains[GEN_ES_PLUGIN_TOOLCHAIN].proto @@ -41,38 +37,46 @@ def _js_proto_aspect_impl(target, ctx): for src in target[ProtoInfo].direct_sources: proto_file_basename = src.basename.replace(".proto", "") - js_output = ctx.actions.declare_file(GEN_ES_OUTPUT_FILE.format(proto_file_basename = proto_file_basename)) - dts_output = ctx.actions.declare_file(GEN_ES_TYPINGS_OUTPUT_FILE.format(proto_file_basename = proto_file_basename)) - - # Follow https://www.npmjs.com/package/@bufbuild/protoc-gen-es - # Create an action like - # bazel-out/k8-opt-exec-2B5CBBC6/bin/external/com_google_protobuf/protoc $@' '' \ - # '--plugin=protoc-gen-es=bazel-out/k8-opt-exec-2B5CBBC6/bin/plugin/bufbuild/protoc-gen-es.sh' \ - # '--es_opt=keep_empty_files=true' '--es_opt=target=ts' \ - # '--es_out=bazel-out/k8-fastbuild/bin' \ - # '--descriptor_set_in=bazel-out/k8-fastbuild/bin/external/com_google_protobuf/timestamp_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/thing/thing_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/place/place_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/person/person_proto-descriptor-set.proto.bin' \ - # example/person/person.proto - args = ctx.actions.args() - args.add_joined(["--plugin", "protoc-gen-es", gen_es_info.plugin.executable], join_with = "=") - args.add_joined(["--es_out", ctx.bin_dir.path], join_with = "=") - args.add_all(gen_es_info.out_replacement_format_flag.split(" ")) - args.add("--descriptor_set_in") - args.add_joined(target[ProtoInfo].transitive_descriptor_sets, join_with = ctx.configuration.host_path_separator) - args.add_all(target[ProtoInfo].direct_sources) - ctx.actions.run( - executable = protoc_info.proto_compiler.executable, - arguments = [args], - progress_message = "Generating .js/.d.ts from %{label}", - mnemonic = "ProtocGenEs", - env = {"BAZEL_BINDIR": ctx.bin_dir.path}, - tools = [gen_es_info.plugin, protoc_info.proto_compiler], - inputs = depset(target[ProtoInfo].direct_sources, transitive = [target[ProtoInfo].transitive_descriptor_sets]), - outputs = [js_output, dts_output], - use_default_shell_env = True, + js_output = ctx.actions.declare_file( + GEN_ES_OUTPUT_FILE.format(proto_file_basename = proto_file_basename), + sibling = src, + ) + dts_output = ctx.actions.declare_file( + GEN_ES_TYPINGS_OUTPUT_FILE.format(proto_file_basename = proto_file_basename), + sibling = src, ) js_outputs.append(js_output) dts_outputs.append(dts_output) + # Follow https://www.npmjs.com/package/@bufbuild/protoc-gen-es + # Create an action like + # bazel-out/k8-opt-exec-2B5CBBC6/bin/external/com_google_protobuf/protoc $@' '' \ + # '--plugin=protoc-gen-es=bazel-out/k8-opt-exec-2B5CBBC6/bin/plugin/bufbuild/protoc-gen-es.sh' \ + # '--es_opt=keep_empty_files=true' '--es_opt=target=ts' \ + # '--es_out=bazel-out/k8-fastbuild/bin' \ + # '--descriptor_set_in=bazel-out/k8-fastbuild/bin/external/com_google_protobuf/timestamp_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/thing/thing_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/place/place_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/person/person_proto-descriptor-set.proto.bin' \ + # example/person/person.proto + args = ctx.actions.args() + args.add(gen_es_info.plugin.executable, format = "--plugin=protoc-gen-es=%s") + + # args.add_joined(["--es_out", ctx.bin_dir.path], join_with = "=") + args.add_joined(["--es_out", js_outputs[0].dirname], join_with = "=") + args.add_all(gen_es_info.out_replacement_format_flag.split(" ")) + args.add("--descriptor_set_in") + args.add_joined(target[ProtoInfo].transitive_descriptor_sets, join_with = ctx.configuration.host_path_separator) + args.add_all(target[ProtoInfo].direct_sources) + ctx.actions.run( + executable = protoc_info.proto_compiler.executable, + arguments = [args], + progress_message = "Generating .js/.d.ts from %{label}", + mnemonic = "ProtocGenEs", + env = {"BAZEL_BINDIR": ctx.bin_dir.path}, + tools = [gen_es_info.plugin, protoc_info.proto_compiler], + inputs = depset(target[ProtoInfo].direct_sources, transitive = [target[ProtoInfo].transitive_descriptor_sets]), + outputs = js_outputs + dts_outputs, + use_default_shell_env = True, + ) + return [ js_info( target = ctx.label, @@ -98,90 +102,3 @@ js_proto_aspect = aspect( PROTOC_TOOLCHAIN, ], ) - -# def _windows_path_normalize(path): -# """Changes forward slashs to backslashs for Windows paths.""" -# host_is_windows = platform_utils.host_platform_is_windows() -# if host_is_windows: -# return path.replace("/", "\\") -# return path - -# # Vendored: https://github.com/protocolbuffers/protobuf/blob/v31.1/bazel/common/proto_common.bzl#L15-L23 -# 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 - -# # Vendored: https://github.com/protocolbuffers/protobuf/blob/v31.1/bazel/common/proto_common.bzl#L25-L34 -# 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 - -# # Vendored: https://github.com/protocolbuffers/protobuf/blob/v31.1/bazel/common/proto_common.bzl#L36-L46 -# 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 - -# # buildifier: disable=function-docstring-header -# def _protoc_action(ctx, proto_info, outputs): -# - -# # ensure that bin_dir doesn't get duplicated in the path -# # e.g. by proto_library(strip_import_prefix=...) -# proto_root = proto_info.proto_source_root -# if proto_root.startswith(ctx.bin_dir.path): -# proto_root = proto_root[len(ctx.bin_dir.path) + 1:] -# plugin_output = ctx.bin_dir.path + "/" + proto_root - -# # 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 = _import_virtual_proto_path) -# args.add_all(proto_info.transitive_proto_path, map_each = _import_repo_proto_path) -# args.add_all(proto_info.transitive_proto_path, map_each = _import_main_output_proto_path) -# args.add("-I.") # Needs to come last - -# def _declare_outs(ctx, info, ext): -# outs = proto_common.declare_generated_files(ctx.actions, info, "_pb" + ext) -# if ctx.attr.gen_connect_es: -# outs.extend(proto_common.declare_generated_files(ctx.actions, info, "_connect" + ext)) -# if ctx.attr.gen_connect_query: -# proto_sources = info.direct_sources -# proto_source_map = {src.basename: src for src in proto_sources} - -# # FIXME: we should refer to source files via labels instead of filenames -# for proto, services in ctx.attr.gen_connect_query_service_mapping.items(): -# if not proto in proto_source_map: -# fail("{} is not provided by proto_srcs".format(proto)) -# src = proto_source_map.get(proto) -# prefix = proto.replace(".proto", "") -# for service in services: -# outs.append(ctx.actions.declare_file("{}-{}_connectquery{}".format(prefix, service, ext), sibling = src)) - -# return outs diff --git a/js/proto.bzl b/js/proto.bzl index 67efd8ce3b..fb87a9fd40 100644 --- a/js/proto.bzl +++ b/js/proto.bzl @@ -31,40 +31,3 @@ def js_proto_toolchain(name, protoc_plugin, runtime, command_line = ""): command_line = command_line, runtime = runtime, ) - -# FIXME: still want to have .d.ts files placed in the source tree, by diff.bzl -# if not copy_files: -# return -# if not files_to_copy: -# if not proto_srcs: -# fail("Either proto_srcs should be set, or copy_files should be False") -# files_to_copy = [s.replace(".proto", "_pb.d.ts") for s in proto_srcs] -# if gen_connect_es: -# files_to_copy.extend([s.replace(".proto", "_connect.d.ts") for s in proto_srcs]) -# if gen_connect_query: -# for proto, services in gen_connect_query_service_mapping.items(): -# files_to_copy.extend([proto.replace(".proto", "-{}_connectquery.d.ts".format(s)) for s in services]) - -# files_target = "_{}.filegroup".format(name) -# dir_target = "_{}.directory".format(name) -# copy_target = "{}.copy".format(name) - -# native.filegroup( -# name = files_target, -# srcs = [name], -# output_group = "types", -# ) - -# copy_to_directory( -# name = dir_target, -# srcs = [files_target], -# root_paths = ["**"], -# ) - -# write_source_files( -# name = copy_target, -# files = { -# f: make_directory_path("_{}_dirpath".format(f), dir_target, f) -# for f in files_to_copy -# }, -# ) From 89dc20bf1401f359cb84a1f38367c50dc8e83420 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Wed, 11 Feb 2026 13:44:59 -0800 Subject: [PATCH 4/6] refactor: generalize --- js/private/BUILD.bazel | 5 +++- js/private/proto.bzl | 58 ++++++++++++++----------------------- js/private/proto_common.bzl | 58 +++++++++++++++++++++++++++++++++++++ js/proto.bzl | 21 ++------------ 4 files changed, 86 insertions(+), 56 deletions(-) create mode 100644 js/private/proto_common.bzl diff --git a/js/private/BUILD.bazel b/js/private/BUILD.bazel index 8fae2f160b..ba22c026df 100644 --- a/js/private/BUILD.bazel +++ b/js/private/BUILD.bazel @@ -24,7 +24,10 @@ bzl_library( bzl_library( name = "proto", - srcs = ["proto.bzl"], + srcs = [ + "proto.bzl", + "proto_common.bzl", + ], visibility = ["//visibility:public"], deps = [ ":js_helpers", diff --git a/js/private/proto.bzl b/js/private/proto.bzl index c0a18b48f9..a161e22093 100644 --- a/js/private/proto.bzl +++ b/js/private/proto.bzl @@ -17,59 +17,45 @@ load( "gather_transitive_sources", "gather_transitive_types", ) +load(":proto_common.bzl", "proto_common") GEN_ES_PLUGIN_TOOLCHAIN = Label("//js/toolchains:protoc_plugin") PROTOC_TOOLCHAIN = Label("@protobuf//bazel/private:proto_toolchain_type") -GEN_ES_OUTPUT_FILE = "{proto_file_basename}_pb.js" -GEN_ES_TYPINGS_OUTPUT_FILE = "{proto_file_basename}_pb.d.ts" -# FIXME: support stripping import prefixes def _js_proto_aspect_impl(target, ctx): - # Skip generation for well-known-types (is this the right way??) - # if target.label.workspace_root.startswith("external/protobuf"): - # return js_info(target = target.label) - protoc_info = ctx.toolchains[PROTOC_TOOLCHAIN].proto gen_es_info = ctx.toolchains[GEN_ES_PLUGIN_TOOLCHAIN].proto deps = [gen_es_info.runtime] - js_outputs = [] - dts_outputs = [] - - for src in target[ProtoInfo].direct_sources: - proto_file_basename = src.basename.replace(".proto", "") - js_output = ctx.actions.declare_file( - GEN_ES_OUTPUT_FILE.format(proto_file_basename = proto_file_basename), - sibling = src, - ) - dts_output = ctx.actions.declare_file( - GEN_ES_TYPINGS_OUTPUT_FILE.format(proto_file_basename = proto_file_basename), - sibling = src, - ) - js_outputs.append(js_output) - dts_outputs.append(dts_output) + js_outputs = proto_common.declare_generated_files(ctx.actions, target[ProtoInfo], "_pb.js") + dts_outputs = proto_common.declare_generated_files(ctx.actions, target[ProtoInfo], "_pb.d.ts") - # Follow https://www.npmjs.com/package/@bufbuild/protoc-gen-es - # Create an action like - # bazel-out/k8-opt-exec-2B5CBBC6/bin/external/com_google_protobuf/protoc $@' '' \ - # '--plugin=protoc-gen-es=bazel-out/k8-opt-exec-2B5CBBC6/bin/plugin/bufbuild/protoc-gen-es.sh' \ - # '--es_opt=keep_empty_files=true' '--es_opt=target=ts' \ - # '--es_out=bazel-out/k8-fastbuild/bin' \ - # '--descriptor_set_in=bazel-out/k8-fastbuild/bin/external/com_google_protobuf/timestamp_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/thing/thing_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/place/place_proto-descriptor-set.proto.bin:bazel-out/k8-fastbuild/bin/example/person/person_proto-descriptor-set.proto.bin' \ - # example/person/person.proto args = ctx.actions.args() - args.add(gen_es_info.plugin.executable, format = "--plugin=protoc-gen-es=%s") - - # args.add_joined(["--es_out", ctx.bin_dir.path], join_with = "=") - args.add_joined(["--es_out", js_outputs[0].dirname], join_with = "=") - args.add_all(gen_es_info.out_replacement_format_flag.split(" ")) + args.add(gen_es_info.plugin.executable, format = gen_es_info.plugin_format_flag) + proto_outdir = proto_common.output_directory(target[ProtoInfo], js_outputs[0].root) + args.add_all((gen_es_info.out_replacement_format_flag % proto_outdir).split(" ")) args.add("--descriptor_set_in") args.add_joined(target[ProtoInfo].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(target[ProtoInfo].transitive_proto_path, map_each = proto_common.import_virtual_proto_path) + args.add_all(target[ProtoInfo].transitive_proto_path, map_each = proto_common.import_repo_proto_path) + args.add_all(target[ProtoInfo].transitive_proto_path, map_each = proto_common.import_main_output_proto_path) + args.add("-I.") # Needs to come last + args.add_all(target[ProtoInfo].direct_sources) ctx.actions.run( executable = protoc_info.proto_compiler.executable, arguments = [args], progress_message = "Generating .js/.d.ts from %{label}", - mnemonic = "ProtocGenEs", + mnemonic = "JsProtocGenerate", env = {"BAZEL_BINDIR": ctx.bin_dir.path}, tools = [gen_es_info.plugin, protoc_info.proto_compiler], inputs = depset(target[ProtoInfo].direct_sources, transitive = [target[ProtoInfo].transitive_descriptor_sets]), 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 index fb87a9fd40..5a1f1ae3ae 100644 --- a/js/proto.bzl +++ b/js/proto.bzl @@ -8,26 +8,9 @@ See load("@protobuf//bazel/toolchains:proto_lang_toolchain.bzl", "proto_lang_toolchain") load("//js/private:proto.bzl", "GEN_ES_PLUGIN_TOOLCHAIN") -def js_proto_toolchain(name, protoc_plugin, runtime, command_line = ""): - """Declare a toolchain for the TypeScript gRPC protoc plugin. - - NB: the toolchain produced by this macro is actually named [name]_toolchain, so THAT is what you must register. - Even better, make a dedicated 'toolchains' directory and put all your toolchains in there, then register them all with 'register_toolchains("//path/to/toolchains:all")'. - name: any distinct target name, not used - plugin: the protoc plugin to use, typically @bufbuild/protoc-gen-es: - - load("@npm//tools:@bufbuild/protoc-gen-es/package_json.bzl", gen_es = "bin") - gen_es.protoc_gen_es_binary( - name = "protoc_gen_es", - ) - - runtime: dependency that the generated stub code takes, e.g. "//:node_modules/@bufbuild/protobuf" - command_line: command line arguments to pass to the protoc compiler, like "--es_opt=keep_empty_files=true --es_opt=target=js+dts --es_opt=import_extension=js" - """ +def js_proto_toolchain(name, **kwargs): proto_lang_toolchain( name = name, - plugin = protoc_plugin, toolchain_type = GEN_ES_PLUGIN_TOOLCHAIN, - command_line = command_line, - runtime = runtime, + **kwargs ) From c7d0dbee3d2c989fe707bd23c8d5d87c341140cf Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Wed, 11 Feb 2026 14:02:03 -0800 Subject: [PATCH 5/6] now it is beautiful --- js/private/proto.bzl | 40 +++++++++++++++++++++------------------- js/proto.bzl | 4 ++-- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/js/private/proto.bzl b/js/private/proto.bzl index a161e22093..5f0e4d7f5f 100644 --- a/js/private/proto.bzl +++ b/js/private/proto.bzl @@ -19,22 +19,23 @@ load( ) load(":proto_common.bzl", "proto_common") -GEN_ES_PLUGIN_TOOLCHAIN = Label("//js/toolchains:protoc_plugin") +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 - gen_es_info = ctx.toolchains[GEN_ES_PLUGIN_TOOLCHAIN].proto - deps = [gen_es_info.runtime] - js_outputs = proto_common.declare_generated_files(ctx.actions, target[ProtoInfo], "_pb.js") - dts_outputs = proto_common.declare_generated_files(ctx.actions, target[ProtoInfo], "_pb.d.ts") + 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(gen_es_info.plugin.executable, format = gen_es_info.plugin_format_flag) - proto_outdir = proto_common.output_directory(target[ProtoInfo], js_outputs[0].root) - args.add_all((gen_es_info.out_replacement_format_flag % proto_outdir).split(" ")) + 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(target[ProtoInfo].transitive_descriptor_sets, join_with = ctx.configuration.host_path_separator) + 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 @@ -45,20 +46,21 @@ def _js_proto_aspect_impl(target, ctx): # 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(target[ProtoInfo].transitive_proto_path, map_each = proto_common.import_virtual_proto_path) - args.add_all(target[ProtoInfo].transitive_proto_path, map_each = proto_common.import_repo_proto_path) - args.add_all(target[ProtoInfo].transitive_proto_path, map_each = proto_common.import_main_output_proto_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(target[ProtoInfo].direct_sources) + 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": ctx.bin_dir.path}, - tools = [gen_es_info.plugin, protoc_info.proto_compiler], - inputs = depset(target[ProtoInfo].direct_sources, transitive = [target[ProtoInfo].transitive_descriptor_sets]), + 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, ) @@ -70,8 +72,8 @@ def _js_proto_aspect_impl(target, ctx): 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 = deps), - npm_package_store_infos = gather_npm_package_store_infos(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]), ), ] @@ -84,7 +86,7 @@ js_proto_aspect = aspect( # Be a valid dependency of a ts_project rule provides = [JsInfo], toolchains = [ - GEN_ES_PLUGIN_TOOLCHAIN, + LANG_PROTO_TOOLCHAIN, PROTOC_TOOLCHAIN, ], ) diff --git a/js/proto.bzl b/js/proto.bzl index 5a1f1ae3ae..f0af9d982e 100644 --- a/js/proto.bzl +++ b/js/proto.bzl @@ -6,11 +6,11 @@ See """ load("@protobuf//bazel/toolchains:proto_lang_toolchain.bzl", "proto_lang_toolchain") -load("//js/private:proto.bzl", "GEN_ES_PLUGIN_TOOLCHAIN") +load("//js/private:proto.bzl", "LANG_PROTO_TOOLCHAIN") def js_proto_toolchain(name, **kwargs): proto_lang_toolchain( name = name, - toolchain_type = GEN_ES_PLUGIN_TOOLCHAIN, + toolchain_type = LANG_PROTO_TOOLCHAIN, **kwargs ) From 24099b340cc6c3488d99e8288c38b419ec7d97f0 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Wed, 11 Feb 2026 14:34:50 -0800 Subject: [PATCH 6/6] fix bzl_library --- js/private/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/js/private/BUILD.bazel b/js/private/BUILD.bazel index ba22c026df..d67d36401e 100644 --- a/js/private/BUILD.bazel +++ b/js/private/BUILD.bazel @@ -32,6 +32,7 @@ bzl_library( deps = [ ":js_helpers", "//js:providers", + "@protobuf//bazel/common:proto_common_bzl", "@protobuf//bazel/common:proto_info_bzl", ], )