Summary
Our web frontend (app/web) generates gRPC clients with protoc-gen-grpc-web, which emits CommonJS — 64 proto/*_pb.js + *_grpc_web_pb.js files. This keeps the app as a CommonJS package (no "type": "module") and forces build/test shims every time we adopt an ESM-only dependency.
This surfaced in #9236 (Temporal adoption), which required two workarounds purely for ESM/CJS interop:
transpilePackages in next.config.js
- a custom
jest.resolver.js (to request the import export condition)
Why now
ESM-only packages are increasingly common (e.g. temporal-polyfill). The official grpc-web generator has no real ESM support — grpc/grpc-web#535 has been open for years — so we can't fix this by bumping a version or flipping a flag. The ecosystem's modern, ESM-native answer is Buf's Connect-ES + Protobuf-ES.
Proposed direction
- Replace
protoc-gen-grpc-web with protoc-gen-es (+ protoc-gen-connect-es).
- Use
@connectrpc/connect-web's createGrpcWebTransport — this speaks the same gRPC-web wire protocol, so no backend / Envoy changes needed.
- Result: native ESM generated code (tree-shakeable, TS-first), removes all 64 CommonJS proto files, and unblocks moving
app/web to "type": "module".
Scope / cost (not a version bump — a migration)
- ~319
proto/* import sites across the app, plus the service/*.ts layer.
- Message-construction API changes:
const r = new Foo(); r.setBar(x) (google-protobuf) → Protobuf-ES field style. Sweeping, mostly mechanical.
- Recommend a spike first: migrate ONE service end-to-end to size the full effort before committing.
Notes
- No backend change required (gRPC-web transport retained).
- Separately/independently: the Temporal polyfill shims (
transpilePackages, jest.resolver.js) can be deleted once native Temporal reaches Baseline (~mid-2026). Smaller, standalone cleanup.
References
Summary
Our web frontend (
app/web) generates gRPC clients with protoc-gen-grpc-web, which emits CommonJS — 64proto/*_pb.js+*_grpc_web_pb.jsfiles. This keeps the app as a CommonJS package (no"type": "module") and forces build/test shims every time we adopt an ESM-only dependency.This surfaced in #9236 (Temporal adoption), which required two workarounds purely for ESM/CJS interop:
transpilePackagesinnext.config.jsjest.resolver.js(to request theimportexport condition)Why now
ESM-only packages are increasingly common (e.g.
temporal-polyfill). The officialgrpc-webgenerator has no real ESM support — grpc/grpc-web#535 has been open for years — so we can't fix this by bumping a version or flipping a flag. The ecosystem's modern, ESM-native answer is Buf's Connect-ES + Protobuf-ES.Proposed direction
protoc-gen-grpc-webwithprotoc-gen-es(+protoc-gen-connect-es).@connectrpc/connect-web'screateGrpcWebTransport— this speaks the same gRPC-web wire protocol, so no backend / Envoy changes needed.app/webto"type": "module".Scope / cost (not a version bump — a migration)
proto/*import sites across the app, plus theservice/*.tslayer.const r = new Foo(); r.setBar(x)(google-protobuf) → Protobuf-ES field style. Sweeping, mostly mechanical.Notes
transpilePackages,jest.resolver.js) can be deleted once native Temporal reaches Baseline (~mid-2026). Smaller, standalone cleanup.References