From 8763c488cbc16704bb51706645ecfa8328d00d82 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Thu, 12 Mar 2026 12:24:05 -0700 Subject: [PATCH 1/2] feat: add example of using proto_library with Node.js (#2767) Also fix a bug where `js_library` deps ran the proto aspect, but js_binary/js_test did not. /cc @acozzette --- examples/proto/.bazelrc | 1 + examples/proto/BUILD.bazel | 14 +++++++------ examples/proto/status_pb.d.ts | 29 ++++++++++++++++++++++++++ examples/proto/user_pb.d.ts | 39 +++++++++++++++++++++++++++++++++++ js/BUILD.bazel | 2 ++ js/private/js_binary.bzl | 2 ++ js/proto.bzl | 5 ++++- 7 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 examples/proto/status_pb.d.ts create mode 100644 examples/proto/user_pb.d.ts diff --git a/examples/proto/.bazelrc b/examples/proto/.bazelrc index 8484fe48a3..6a2136b81c 100644 --- a/examples/proto/.bazelrc +++ b/examples/proto/.bazelrc @@ -1,2 +1,3 @@ build --incompatible_enable_proto_toolchain_resolution build --@protobuf//bazel/toolchains:prefer_prebuilt_protoc +build --@diff.bzl//diff:validate diff --git a/examples/proto/BUILD.bazel b/examples/proto/BUILD.bazel index a60d6d72ac..86e41f37cb 100644 --- a/examples/proto/BUILD.bazel +++ b/examples/proto/BUILD.bazel @@ -1,4 +1,5 @@ -load("@aspect_rules_js//js:defs.bzl", "js_library", "js_test") +load("@aspect_rules_js//js:defs.bzl", "js_test") +load("@aspect_rules_js//js:proto.bzl", "js_proto_library") load("@npm//:defs.bzl", "npm_link_all_packages") load("@protobuf//bazel:proto_library.bzl", "proto_library") @@ -13,14 +14,15 @@ proto_library( deps = ["@protobuf//:timestamp_proto"], ) -js_library( - name = "foo_js_lib", - # This edge runs an aspect to produce a JsInfo provider with protobuf .js/.d.ts files. - deps = [":foo_proto"], +js_proto_library( + name = "foo_js_proto", + copy_types = ["{}_pb.d.ts".format(name.replace(".proto", "")) for name in glob(["*.proto"])], + # An aspect is attached to this edge to produce a JsInfo provider + proto = ":foo_proto", ) js_test( name = "test", - data = [":foo_js_lib"], + data = [":foo_js_proto"], entry_point = "test_proto.js", ) diff --git a/examples/proto/status_pb.d.ts b/examples/proto/status_pb.d.ts new file mode 100644 index 0000000000..0b76599044 --- /dev/null +++ b/examples/proto/status_pb.d.ts @@ -0,0 +1,29 @@ +// @generated by protoc-gen-es v2.2.5 with parameter "keep_empty_files=true,target=js+dts,import_extension=js" +// @generated from file status.proto (package status, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; +import type { Message } from "@bufbuild/protobuf"; +import type { Timestamp } from "@bufbuild/protobuf/wkt"; + +/** + * Describes the file status.proto. + */ +export declare const file_status: GenFile; + +/** + * @generated from message status.Status + */ +export declare type Status = Message<"status.Status"> & { + /** + * @generated from field: google.protobuf.Timestamp created_at = 1; + */ + createdAt?: Timestamp; +}; + +/** + * Describes the message status.Status. + * Use `create(StatusSchema)` to create a new message. + */ +export declare const StatusSchema: GenMessage; + diff --git a/examples/proto/user_pb.d.ts b/examples/proto/user_pb.d.ts new file mode 100644 index 0000000000..a194ef68f7 --- /dev/null +++ b/examples/proto/user_pb.d.ts @@ -0,0 +1,39 @@ +// @generated by protoc-gen-es v2.2.5 with parameter "keep_empty_files=true,target=js+dts,import_extension=js" +// @generated from file user.proto (syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; +import type { Message } from "@bufbuild/protobuf"; +import type { Status } from "./status_pb.js"; + +/** + * Describes the file user.proto. + */ +export declare const file_user: GenFile; + +/** + * @generated from message User + */ +export declare type User = Message<"User"> & { + /** + * @generated from field: string name = 1; + */ + name: string; + + /** + * @generated from field: status.Status status = 2; + */ + status?: Status; + + /** + * @generated from field: repeated string tags = 3; + */ + tags: string[]; +}; + +/** + * Describes the message User. + * Use `create(UserSchema)` to create a new message. + */ +export declare const UserSchema: GenMessage; + diff --git a/js/BUILD.bazel b/js/BUILD.bazel index 06e22df7b6..6b99db2268 100644 --- a/js/BUILD.bazel +++ b/js/BUILD.bazel @@ -28,6 +28,8 @@ bzl_library( visibility = ["//visibility:public"], deps = [ "//js/private:proto", + "@bazel_skylib//rules:select_file", + "@diff.bzl//diff:defs", "@protobuf//bazel/toolchains:proto_lang_toolchain_bzl", ], ) diff --git a/js/private/js_binary.bzl b/js/private/js_binary.bzl index 2c67f7219b..05d1a1c87a 100644 --- a/js/private/js_binary.bzl +++ b/js/private/js_binary.bzl @@ -21,6 +21,7 @@ load("@bazel_lib//lib:expand_make_vars.bzl", "expand_locations", "expand_variabl load("@bazel_lib//lib:windows_utils.bzl", "create_windows_native_launcher_script") load(":bash.bzl", "BASH_INITIALIZE_RUNFILES") load(":js_helpers.bzl", "LOG_LEVELS", "envs_for_log_level", "gather_files_from_js_infos", "gather_runfiles") +load(":proto.bzl", "js_proto_aspect") _DOC = """Execute a program in the Node.js runtime. @@ -99,6 +100,7 @@ _ATTRS = { NB: `data` files are copied to the Bazel output tree before being passed as inputs to runfiles. See `copy_data_to_bin` docstring for more info. """, + aspects = [js_proto_aspect], ), "entry_point": attr.label( allow_files = True, diff --git a/js/proto.bzl b/js/proto.bzl index 6eed3e20d5..1a6fb7cff2 100644 --- a/js/proto.bzl +++ b/js/proto.bzl @@ -43,7 +43,7 @@ The generator you setup earlier will be invoked automatically as an action to ge """ load("@protobuf//bazel/toolchains:proto_lang_toolchain.bzl", "proto_lang_toolchain") -load("//js/private:proto.bzl", "LANG_PROTO_TOOLCHAIN") +load("//js/private:proto.bzl", "LANG_PROTO_TOOLCHAIN", js_proto_library_rule = "js_proto_library") def js_proto_toolchain(name, plugin_name, plugin_options, plugin_bin, runtime, **kwargs): """Define a proto_lang_toolchain that uses the plugin. @@ -97,3 +97,6 @@ def js_proto_toolchain(name, plugin_name, plugin_options, plugin_bin, runtime, * runtime = runtime, **kwargs ) + +# TODO: expand to a macro that copies the types back to the source folder +js_proto_library = js_proto_library_rule From 15b92c70accda55332ae950a977bba0ecf6accbe Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Thu, 12 Mar 2026 15:09:27 -0700 Subject: [PATCH 2/2] restore to working --- MODULE.bazel | 1 + examples/proto/BUILD.bazel | 5 ++++- examples/proto/status.proto | 1 + examples/proto/status_pb.d.ts | 5 +++++ js/private/js_binary.bzl | 2 -- js/private/proto.bzl | 21 +++++++++++++++++++++ js/proto.bzl | 27 +++++++++++++++++++++++++-- 7 files changed, 57 insertions(+), 5 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 9100e7b0f7..1c41793892 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,6 +8,7 @@ module( # Lower-bounds (minimum) versions for direct runtime dependencies. # Do not bump these unless rules_js requires a newer version to function. +bazel_dep(name = "diff.bzl", version = "0.4.3") bazel_dep(name = "tar.bzl", version = "0.6.0") bazel_dep(name = "yq.bzl", version = "0.3.2") bazel_dep(name = "jq.bzl", version = "0.4.0") diff --git a/examples/proto/BUILD.bazel b/examples/proto/BUILD.bazel index 86e41f37cb..959a2b72e0 100644 --- a/examples/proto/BUILD.bazel +++ b/examples/proto/BUILD.bazel @@ -23,6 +23,9 @@ js_proto_library( js_test( name = "test", - data = [":foo_js_proto"], + data = [ + ":foo_js_proto", + ":foo_js_proto.diff", + ], entry_point = "test_proto.js", ) diff --git a/examples/proto/status.proto b/examples/proto/status.proto index d461d79736..9c2ad5cdcf 100644 --- a/examples/proto/status.proto +++ b/examples/proto/status.proto @@ -6,4 +6,5 @@ package status; message Status { google.protobuf.Timestamp created_at = 1; + string message = 2; } diff --git a/examples/proto/status_pb.d.ts b/examples/proto/status_pb.d.ts index 0b76599044..b129ee9e5b 100644 --- a/examples/proto/status_pb.d.ts +++ b/examples/proto/status_pb.d.ts @@ -19,6 +19,11 @@ export declare type Status = Message<"status.Status"> & { * @generated from field: google.protobuf.Timestamp created_at = 1; */ createdAt?: Timestamp; + + /** + * @generated from field: string message = 2; + */ + message: string; }; /** diff --git a/js/private/js_binary.bzl b/js/private/js_binary.bzl index 05d1a1c87a..2c67f7219b 100644 --- a/js/private/js_binary.bzl +++ b/js/private/js_binary.bzl @@ -21,7 +21,6 @@ load("@bazel_lib//lib:expand_make_vars.bzl", "expand_locations", "expand_variabl load("@bazel_lib//lib:windows_utils.bzl", "create_windows_native_launcher_script") load(":bash.bzl", "BASH_INITIALIZE_RUNFILES") load(":js_helpers.bzl", "LOG_LEVELS", "envs_for_log_level", "gather_files_from_js_infos", "gather_runfiles") -load(":proto.bzl", "js_proto_aspect") _DOC = """Execute a program in the Node.js runtime. @@ -100,7 +99,6 @@ _ATTRS = { NB: `data` files are copied to the Bazel output tree before being passed as inputs to runfiles. See `copy_data_to_bin` docstring for more info. """, - aspects = [js_proto_aspect], ), "entry_point": attr.label( allow_files = True, diff --git a/js/private/proto.bzl b/js/private/proto.bzl index 5f0e4d7f5f..39b1d6b2ef 100644 --- a/js/private/proto.bzl +++ b/js/private/proto.bzl @@ -90,3 +90,24 @@ js_proto_aspect = aspect( PROTOC_TOOLCHAIN, ], ) + +def _js_proto_library_impl(ctx): + return [ + OutputGroupInfo(types = ctx.attr.proto[JsInfo].types), + ctx.attr.proto[JsInfo], + ] + +js_proto_library = rule( + doc = """A rule that wraps a proto_library to invoke the js_proto_aspect. + + Useful when you must adapt a ProtoInfo provider to a JsInfo provider, for rule kinds that don't invoke the js_proto_aspect on their deps. + """, + implementation = _js_proto_library_impl, + attrs = { + "proto": attr.label( + mandatory = True, + providers = [ProtoInfo], + aspects = [js_proto_aspect], + ), + }, +) diff --git a/js/proto.bzl b/js/proto.bzl index 1a6fb7cff2..7c5304d2b7 100644 --- a/js/proto.bzl +++ b/js/proto.bzl @@ -42,6 +42,8 @@ js_library( The generator you setup earlier will be invoked automatically as an action to generate the `.js` and `.d.ts` files. """ +load("@bazel_skylib//rules:select_file.bzl", "select_file") +load("@diff.bzl//diff:defs.bzl", "diff") load("@protobuf//bazel/toolchains:proto_lang_toolchain.bzl", "proto_lang_toolchain") load("//js/private:proto.bzl", "LANG_PROTO_TOOLCHAIN", js_proto_library_rule = "js_proto_library") @@ -98,5 +100,26 @@ def js_proto_toolchain(name, plugin_name, plugin_options, plugin_bin, runtime, * **kwargs ) -# TODO: expand to a macro that copies the types back to the source folder -js_proto_library = js_proto_library_rule +def js_proto_library(name, proto, copy_types = []): + """Wrap a proto_library to invoke the js_proto_toolchain. + + Can copy .d.ts type definitions back to the source folder. + + Args: + name: The name of the library. + proto: The proto_library to wrap. + copy_types: HACK to get the label of the file in the source folder. + """ + + js_proto_library_rule(name = name, proto = proto) + if len(copy_types) > 0: + native.filegroup(name = "_{}.types".format(name), srcs = [name], output_group = "types") + for i, src_file in enumerate(copy_types): + gen_file = "_{}.gen_{}.d.ts".format(name, i) + select_file(name = gen_file, srcs = "_{}.types".format(name), subpath = src_file) + diff( + name = "{}.diff_{}".format(name, i), + file1 = src_file, + file2 = gen_file, + ) + native.filegroup(name = "{}.diff".format(name), srcs = ["{}.diff_{}".format(name, i) for i in range(len(copy_types))])