blob: 9963022050c28320c8de4508b395054cea2e6265 [file]
"""Rule for generating JavaScript and TypeScript RPC stubs from proto_library targets.
This integrates protoc RPC plugins (such as protoc-gen-connect-query) with Bazel,
following the "Local Generation" mechanism described at
https://connectrpc.com/docs/web/generating-code#local-generation.
"""
load("@protobuf//bazel/common:proto_info.bzl", "ProtoInfo")
load("//js:providers.bzl", "js_info")
load(
"//js/private:js_helpers.bzl",
"gather_npm_package_store_infos",
"gather_npm_sources",
)
load(":proto.bzl", "PROTOC_TOOLCHAIN")
load(":proto_common.bzl", "proto_common")
LANG_RPC_TOOLCHAIN = Label("//js/toolchains:protoc_rpc_plugin")
def _js_rpc_library_impl(ctx):
protoc_info = ctx.toolchains[PROTOC_TOOLCHAIN].proto
proto_lang_toolchain_info = ctx.toolchains[LANG_RPC_TOOLCHAIN].proto
js_proto_toolchain_info = ctx.toolchains[LANG_RPC_TOOLCHAIN].js
if len(ctx.attr.deps) != 1:
fail("There must be exactly one dependency in deps.")
dep_label = ctx.attr.deps[0].label
if dep_label.package != ctx.label.package:
fail("js_rpc_library '%s' may only depend on a proto_library in the same package, and '%s' is in a different package." % (ctx.label, dep_label))
proto_info = ctx.attr.deps[0][ProtoInfo]
js_extension = js_proto_toolchain_info.out_js_extension
dts_extension = js_proto_toolchain_info.out_dts_extension
if not ctx.attr.outs and not js_extension and not dts_extension:
fail("No outputs were specified.")
if ctx.attr.outs and (js_extension or dts_extension):
fail("Outputs can be specified on the js_rpc_library or js_rpc_toolchain, but not both.")
if not proto_info.direct_sources:
fail("The proto_library must have at least one .proto file source.")
output_files = []
if ctx.attr.outs:
output_files.extend([ctx.actions.declare_file(f, sibling = proto_info.direct_sources[0]) for f in ctx.attr.outs])
if js_proto_toolchain_info.out_js_extension:
output_files.extend(proto_common.declare_generated_files(ctx.actions, proto_info, js_proto_toolchain_info.out_js_extension))
if js_proto_toolchain_info.out_dts_extension:
output_files.extend(proto_common.declare_generated_files(ctx.actions, proto_info, js_proto_toolchain_info.out_dts_extension))
proto_common.compile(
actions = ctx.actions,
proto_lang_toolchain_info = proto_lang_toolchain_info,
protoc_info = protoc_info,
generated_files = output_files,
proto_info = proto_info,
mnemonic = "JsRpcProtocGenerate",
progress_message = "Generating RPC stubs from %{label}",
host_path_separator = ctx.configuration.host_path_separator,
)
js_outputs = depset([f for f in output_files if not f.path.endswith(".d.ts")])
dts_outputs = depset([f for f in output_files if f.path.endswith(".d.ts")])
return [
js_info(
target = ctx.label,
sources = js_outputs,
types = dts_outputs,
transitive_sources = js_outputs,
transitive_types = dts_outputs,
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]),
),
DefaultInfo(
files = js_outputs,
),
]
js_rpc_library = rule(
implementation = _js_rpc_library_impl,
doc = """Generates RPC client stubs from `proto_library` targets using a configurable protoc plugin.
Runs a protoc plugin (such as `protoc-gen-connect-query`) on the specified `proto_library`
targets and provides the generated files as `JsInfo`, allowing them to be used as deps
in any rule that accepts `JsInfo` (`js_library`, `js_binary`, `js_test`, etc.).
Unlike `js_library` with proto deps (which uses `js_proto_aspect` to generate message
serialization code), `js_rpc_library` operates only on its immediate deps and generates
RPC-specific output such as TanStack Query hooks. The expected output filenames must
be declared explicitly in the `outs` attribute; this is required because some plugins
(like `protoc-gen-connect-query`) generate a variable number of files depending on the
number of service definitions in the proto file.
Note that targets using `js_rpc_library` also need access to the message types from
the same proto files, which are provided by a `js_library` that lists the same
`proto_library` targets in its deps.
Example:
```starlark
load("@aspect_rules_js//js:defs.bzl", "js_library", "js_test")
load("@aspect_rules_js//js:rpc.bzl", "js_rpc_library")
load("@protobuf//bazel:proto_library.bzl", "proto_library")
proto_library(
name = "eliza_proto",
srcs = ["eliza.proto"],
)
# Generates message serialization code (eliza_pb.js) via js_proto_aspect
js_library(
name = "eliza_js",
deps = [":eliza_proto"],
)
# Generates one file per service definition in eliza.proto
js_rpc_library(
name = "eliza_rpc",
deps = [":eliza_proto"],
outs = [
"eliza-ElizaService_connectquery.js",
"eliza-ElizaService_connectquery.d.ts",
],
)
js_test(
name = "test",
entry_point = "test.js",
data = [":eliza_js", ":eliza_rpc"],
)
```
""",
attrs = {
"deps": attr.label_list(
doc = """`proto_library` targets whose sources will be passed to the RPC plugin.
There must be exactly one target in this list.""",
providers = [ProtoInfo],
),
"outs": attr.string_list(
doc = """Explicit list of files the plugin will generate, relative to the package directory.
This parameter must be specified if and only if the registered `js_rpc_toolchain`
does not specify `out_js_extension` or `out_dts_extension`.
These must match exactly the files that the plugin will emit. For `protoc-gen-connect-query`,
one file per service is generated with the naming pattern:
`{proto_name}-{ServiceName}_connectquery.{js,d.ts}`
For example, for `eliza.proto` containing `service ElizaService`:
`outs = ["eliza-ElizaService_connectquery.js", "eliza-ElizaService_connectquery.d.ts"]`
""",
),
},
toolchains = [
LANG_RPC_TOOLCHAIN,
PROTOC_TOOLCHAIN,
],
)