feat(ts): protobuf-es runtime mode for TS client/server (native messages + sebuf transport)#207
Draft
hishamank wants to merge 13 commits into
Draft
feat(ts): protobuf-es runtime mode for TS client/server (native messages + sebuf transport)#207hishamank wants to merge 13 commits into
hishamank wants to merge 13 commits into
Conversation
…ess proof Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reject unknown parameter keys and invalid ts_runtime values in the plugin ParamFunc so a typo fails loudly instead of silently falling back to hand-rolled output. Collapse the ParseMessageRuntime pass- through wrapper into a single exported function. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oreUnknownFields) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend ts_runtime=protobuf-es to server-streaming (SSE) RPCs: the async
generator now takes a MessageInitShape request, encodes any request body via
create/toJson, and decodes each streamed event through
fromJson(<Res>Schema, JSON.parse(data), { ignoreUnknownFields: true }) instead
of a raw cast, importing schemas/types from protoc-gen-es <proto>_pb.js.
Derive protoc-gen-es local symbol names via ESQualifiedName (underscore-joined
nested names, e.g. Outer_Inner / Outer_InnerSchema) rather than QualifiedTSName,
matching @bufbuild/protoc-gen-es v2.12.1 output (verified empirically).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nknown fields ignored) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…onses) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…afe) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enum path/query parameters (including repeated enum query params) are not representable in protobuf-es mode: enums are numeric there but URL params arrive as strings, and RefEnum resolves to the hand-rolled ./<proto>.js type module that es-mode deliberately does not emit -> downstream TS2307. Instead of emitting uncompilable output, both generators now reject enum path/query params at generation time via checkNoEnumParamsES with a clear error. Adds a non-enum unary GET golden (string path param + scalar query params, get_params.proto) wired into the es golden tests and typechecked under --strict --noUnusedLocals, plus in-process tests asserting the fail-loud path. Updates docs to cover enum path AND query params. Removes obsolete TODO(es). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The @bufbuild/protobuf import list was derived by regex word-boundary
scanning of the generated body, which had two failure modes:
- an RPC named Create renders `async create(`, injecting
`import { create } from "@bufbuild/protobuf"` into HAND-ROLLED output,
whose consumers do not install that package;
- a field named create in an es-mode GET method (`req.create`) imported
the helper unused, failing consumers under noUnusedLocals.
Each es-mode emission site now records exactly the helpers it prints
(MessageInitShape/fromJson per method; create/toJson only when a body or
response is encoded; create for the server GET init wrap), the body scan
is deleted, and EmitContext.NeedProtobufES is a no-op outside protobuf-es
runtime mode as a structural invariant. Existing goldens are
byte-identical; regression tests cover both failure modes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Byte-comparing goldens locks down what sebuf emits but not that it compiles against the real @bufbuild/protobuf types and the symbols protoc-gen-es actually exported — the check that catches an ESQualifiedName naming divergence in CI instead of a consumer build. Ports the tscommon/typecheck harness (strict nodenext + noUnusedLocals, tsc from PATH or pinned via npx, skips when unavailable) and runs it over testdata/golden/es in both generators, resolving @bufbuild/protobuf through the same git-ignored node_modules discovery the wire-conformance test uses (SEBUF_ES_NODE_MODULES override supported). Also checks in the transitive sebuf/http/annotations_pb.ts goldens the fixtures' *_pb.ts import — without them the es golden tree never typechecked (TS2307 on ./sebuf/http/annotations_pb.js). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
URL path/query parameters do not route through create/toJson/fromJson — only request bodies, response bodies, and SSE events do — and MessageInitShape makes every request field optional, so a missing path parameter compiles and surfaces only as a runtime "undefined" URL segment. Also notes the es golden typecheck tests as the guard for protoc-gen-es naming drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds an opt-in
ts_runtime=protobuf-esmode toprotoc-gen-ts-clientandprotoc-gen-ts-server. In this mode, message types + JSON codec come from Buf's nativeprotoc-gen-es(*_pb.ts), and sebuf generates only the HTTP transport on top — routing every request/response throughcreate/toJson/fromJson(..., { ignoreUnknownFields: true }). This mirrors how Go usesprotoc-gen-go+protoc-gen-go-http.The existing hand-rolled mode stays the default — no behavior change unless
ts_runtime=protobuf-esis passed.Why
sebuf's Go output uses the language's native protobuf runtime, so it can't drift from protojson. The TS output hand-rolls message types with no codec, so it can silently disagree with the wire — e.g. a server omits zero-values per protojson, but the hand-rolled TS types declare them present, so
resp.list.map(...)throws on an empty list. This brings TS into the native-runtime tier. It is not adopting Connect — sebuf keeps its own HTTP transport (paths, headers,ValidationError); only the message layer goes native.How it works
ts_runtime=protobuf-esplugin option (invalid values fail loudly).protoc-gen-esowns them) but still emits the sharederrors.ts.<Msg>Schema(value) +<Msg>(type) from./<proto>_pb.js.@bufbuild/protobufimport lists only the symbols actually used: each es-mode emission site records the helpers it prints (MessageInitShape/fromJsonper method,create/toJsononly when a body or response is encoded), so the import mirrors real usage and compiles undernoUnusedLocals.MessageInitShape<typeof ReqSchema>params;body: JSON.stringify(toJson(ReqSchema, create(ReqSchema, req)));fromJson(ResSchema, await resp.json(), { ignoreUnknownFields: true }).fromJsonrequest decode;toJson(create(ResSchema, result))response encode; handler takes branded<Req>, returnsMessageInitShape<...>.Review fixes (second round of commits)
A review pass found two real issues in the first iteration; both are fixed on this branch:
@bufbuild/protobufimport (fixed in8c1e4c1). The runtime-symbol import list was originally derived by regex-scanning the generated body, and the scan ran in both modes. An RPC literally namedCreaterendersasync create(, which matched\bcreate\band injectedimport { create } from "@bufbuild/protobuf"into hand-rolled output — a package hand-rolled consumers don't install. A field namedcreatein an es-mode GET method had the mirror problem: an unused import that failsnoUnusedLocals. The body scan is now deleted; each es-mode emission site records exactly the helpers it prints, andNeedProtobufESis a no-op outside es mode as a structural invariant. Regression tests cover both failure modes (runtime_symbols_test.goin both generators), and all pre-existing goldens are byte-identical under the new tracking.96b33c9). The es goldens' typecheck was a manual verification, and it turned out the checked-in golden tree didn't actually compile: the fixtures'*_pb.tsimport./sebuf/http/annotations_pb.js, which was never checked in (TS2307). NewTestTSClientGenESGoldenTypecheck/TestTSServerGenESGoldenTypecheckruntsc --noEmit(strict nodenext +noUnusedLocals, via the portedtscommon/typecheckharness) overtestdata/golden/esagainst the real@bufbuild/protobufpackage, skipping cleanly when the toolchain is absent. The missingannotations_pb.tsgoldens are now checked in. This is also the machine guard forESQualifiedNamedrift: a symbol-name divergence from protoc-gen-es now fails CI with aTS2724: no exported membererror instead of surfacing in a consumer build (verified by fault injection).Also documented (
5854efd): URL path/query params do not route through the codec (only bodies and SSE events do — by design, URL params have no protojson form), andMessageInitShapemakes all request fields optional, so a missing path param compiles and only surfaces as a runtime"undefined"URL segment.Tests / verification
go test ./..., gofmt, vet, staticcheck all green.testdata/golden/es/).tsc --noEmit, strict nodenext,noUnusedLocals) against the real@bufbuild/protobufpackage — no longer a manual claim.@bufbuild/protobuf(RPC namedCreate) and that es-mode imports only the runtime symbols actually used (GET method with a field namedcreate).[]), and unknown fields don't throw (ignoreUnknownFields) — but do throw without the flag.Known limitations (documented in
docs/client-generation.md)ESQualifiedName, verified against@bufbuild/protoc-gen-es@2.12.1; the es golden typecheck catches drift on other versions).MessageInitShape— see docs.Base
Targets
feat/ts-modules-oneof-integration(the merged #199 + #200 result) since the es transport reuses that modules-layout import machinery. Rebase ontomainonce those land.🤖 Generated with Claude Code