A Protocol Buffers compiler plugin that generates TypeScript code for React Native gRPC clients with binary protobuf serialization support.
- TypeScript Generation: Generates TypeScript interfaces, classes, and enums
- Binary Serialization: Full protobuf wire format support compatible with grpc-react-native
- Streaming Support: All gRPC streaming patterns (unary, server, client, bidirectional)
- Message Classes: Generates classes with
serializeBinary(),deserializeBinary(), andtoObject()methods - Field Type Support: All protobuf field types including repeated fields and maps
- Cross-package Imports: Proper handling of dependencies between packages
- React Native Compatible: Optimized for React Native with binary data handling
The easiest way to build the plugin is using the Makefile:
# Build the plugin (default target)
make
# Or explicitly
make buildThis will create protoc-gen-grpc-react-native in the current directory.
To install the plugin globally so you can use it without specifying the path:
# Install to /usr/local/bin (requires sudo)
sudo make install
# Or install to a custom directory (e.g., ~/bin)
make install INSTALL_DIR=~/binOnce built, use the plugin with protoc:
# Basic usage
protoc --plugin=./protoc-gen-grpc-react-native \
--grpc-react-native_out=./output \
-I./proto \
./proto/service.proto- Go 1.19 or later
- Protocol Buffers compiler (
protoc) - grpc-react-native client library
cd grpc-react-native-plugin
make build
# or
go build -o protoc-gen-grpc-react-native main.goTo use the plugin without specifying the full path each time, you can install it to a directory in your PATH:
# Build the plugin
go build -o protoc-gen-grpc-react-native
# Install to a directory in PATH (e.g., /usr/local/bin or ~/bin)
sudo cp protoc-gen-grpc-react-native /usr/local/bin/
# or
cp protoc-gen-grpc-react-native ~/bin/
# Make sure the directory is in your PATH
export PATH=$PATH:~/binAfter installation, you can use the plugin without the --plugin flag:
protoc --grpc-react-native_out=./output -I./proto ./proto/service.proto- Build the plugin (if not already built):
go build -o protoc-gen-grpc-react-native- Install protoc (if not already installed):
# Ubuntu/Debian
sudo apt-get install protobuf-compiler
# macOS
brew install protobuf
# Or download from: https://github.com/protocolbuffers/protobuf/releasesprotoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./output -I./proto ./proto/your_service.protoprotoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./output -I./proto ./proto/service1.proto ./proto/service2.protoprotoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./output -I./proto ./proto/*.protoprotoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./src/generated -I./proto ./proto/accounting/service.protoprotoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./output -I./proto -I./common ./proto/service.proto# Single service
protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./output -I./neofs-api neofs-api/accounting/service.proto
# All NeoFS services
protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./output -I./neofs-api neofs-api/accounting/service.proto neofs-api/object/service.proto neofs-api/container/service.protoprotoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./output -I./proto ./proto/streaming_service.protoprotoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./output -I./proto ./proto/advanced_types.protoThe plugin generates the following files:
*_types.ts- TypeScript interfaces, classes, enums, and binary serialization*_services.ts- gRPC service clients (only generated if proto contains services)
output/
├── simple_types.ts # Simple message types
├── service_types.ts # Service message types
├── service_services.ts # Service client
├── streaming_service_types.ts # Streaming message types
└── streaming_service_services.ts # Streaming service client
# Basic usage
--grpc-react-native_out=./output
# With custom options (future enhancement)
--grpc-react-native_out=./output:option1=value1,option2=value2# Single include path
-I./proto
# Multiple include paths
-I./proto -I./common -I./third_party
# Relative to current directory
-I. -I./proto -I./neofs-apiPROTO_FILES := $(wildcard proto/*.proto)
GENERATED_FILES := $(PROTO_FILES:.proto=_types.ts) $(PROTO_FILES:.proto=_services.ts)
.PHONY: generate
generate: $(GENERATED_FILES)
%_types.ts %_services.ts: %.proto
protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=. -I./proto $<{
"scripts": {
"generate": "protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./src/generated -I./proto ./proto/*.proto",
"generate:neofs": "protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./src/generated -I./neofs-api neofs-api/accounting/service.proto neofs-api/object/service.proto"
}
}task generateProto {
doLast {
exec {
commandLine 'protoc',
'--plugin=./protoc-gen-grpc-react-native',
'--grpc-react-native_out=./src/generated',
'-I./proto',
'./proto/service.proto'
}
}
}- Plugin not found:
# Make sure plugin is built and executable
chmod +x protoc-gen-grpc-react-native- Import errors:
# Use correct include paths
protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./output -I./proto -I./common ./proto/service.proto- Permission denied:
# Make plugin executable
chmod +x protoc-gen-grpc-react-native- Output directory doesn't exist:
# Create output directory first
mkdir -p output
protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./output -I./proto ./proto/service.proto# Verbose output
protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./output -I./proto --verbose ./proto/service.protoThe plugin generates TypeScript classes with serialization methods:
export class SimpleMessageImpl {
constructor(data?: Partial<SimpleMessage>) {
this.name = data?.name ?? "";
this.age = data?.age ?? 0;
this.active = data?.active ?? false;
}
name!: string;
age!: number;
active!: boolean;
serializeBinary(): Uint8Array {
// Binary protobuf serialization
}
static deserializeBinary(data: Uint8Array): SimpleMessageImpl {
// Binary protobuf deserialization
}
toObject(): SimpleMessage {
// Convert to plain object
}
}The plugin generates service client classes with streaming support:
export class ObjectServiceClient {
constructor(private client: GrpcClient) {}
// Unary call
async delete(request: DeleteRequestImpl): Promise<DeleteResponseImpl> {
const response = await this.client.unaryCall(
'neo.fs.v2.object.ObjectService/Delete',
request.serializeBinary()
);
return DeleteResponseImpl.deserializeBinary(response.data as Uint8Array);
}
// Server streaming
async* get(request: GetRequestImpl): AsyncGenerator<GetResponseImpl> {
const stream = this.client.serverStreamCall(
'neo.fs.v2.object.ObjectService/Get',
request.serializeBinary()
);
for await (const response of stream) {
if (response.done) {
break; // End of stream
}
if (response.data) {
yield GetResponseImpl.deserializeBinary(response.data as Uint8Array);
}
}
}
// Client streaming - accepts AsyncIterable of requests
async put(requests: AsyncIterable<PutRequestImpl>): Promise<PutResponseImpl> {
async function* serialize() {
for await (const req of requests) {
yield req.serializeBinary();
}
}
const response = await this.client.clientStreamCall(
'neo.fs.v2.object.ObjectService/Put',
serialize()
);
return PutResponseImpl.deserializeBinary(response.data as Uint8Array);
}
// Bidirectional streaming
async* chat(requests: AsyncIterable<ChatMessageImpl>): AsyncGenerator<ChatMessageImpl> {
async function* serialize() {
for await (const req of requests) {
yield req.serializeBinary();
}
}
const stream = this.client.bidirectionalStreamCall(
'neo.fs.v2.object.ObjectService/Chat',
serialize()
);
for await (const response of stream) {
if (response.done) {
break; // End of stream
}
if (response.data) {
yield ChatMessageImpl.deserializeBinary(response.data as Uint8Array);
}
}
}
}The plugin generates TypeScript interfaces and enums:
export interface SimpleMessage {
name: string;
age: number;
active: boolean;
score: number;
data: Uint8Array;
}
export enum Status {
STATUS_UNSPECIFIED = 0,
STATUS_ACTIVE = 1,
STATUS_INACTIVE = 2,
}- Scalar Types: int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, float, double, bool, string, bytes
- Repeated Fields: Arrays of any scalar or message type
- Map Fields: Maps with string/numeric keys
- Nested Messages: Proper type references for nested message types
- Enums: TypeScript enums with proper value mappings
- Unary:
async method(request: RequestImpl): Promise<ResponseImpl> - Server Streaming:
async* method(request: RequestImpl): AsyncGenerator<ResponseImpl> - Client Streaming:
async method(requests: AsyncIterable<RequestImpl>): Promise<ResponseImpl> - Bidirectional Streaming:
async* method(requests: AsyncIterable<RequestImpl>): AsyncGenerator<ResponseImpl>
The plugin includes a complete BinaryWriter and BinaryReader implementation:
class BinaryWriter {
writeString(fieldNumber: number, value: string): void
writeBytes(fieldNumber: number, value: Uint8Array): void
writeInt32(fieldNumber: number, value: number): void
writeMessage(fieldNumber: number, value: any): void
// ... more methods
}
class BinaryReader {
readString(fieldNumber: number): string
readBytes(fieldNumber: number): Uint8Array
readInt32(fieldNumber: number): number
readMessage(fieldNumber: number, deserializer: Function): any
// ... more methods
}The generated code is designed to work seamlessly with the grpc-react-native client:
import { GrpcClient } from 'grpc-react-native';
import { AccountingServiceClient } from './generated/accounting_service_services';
import { BalanceRequestImpl } from './generated/accounting_types';
// Create gRPC client
const grpcClient = new GrpcClient({
host: 'localhost',
port: 50051,
useTls: false
});
// Create service client
const accountingClient = new AccountingServiceClient(grpcClient);
// Create request message
const request = new BalanceRequestImpl({
ownerId: "test-owner-id"
});
// Make gRPC call
const response = await accountingClient.balance(request);
console.log('Response:', response);syntax = "proto3";
message SimpleMessage {
string name = 1;
int32 age = 2;
bool active = 3;
double score = 4;
bytes data = 5;
}Generated TypeScript:
export interface SimpleMessage {
name: string;
age: number;
active: boolean;
score: number;
data: Uint8Array;
}
export class SimpleMessageImpl {
constructor(data?: Partial<SimpleMessage>) {
this.name = data?.name ?? "";
this.age = data?.age ?? 0;
this.active = data?.active ?? false;
this.score = data?.score ?? 0.0;
this.data = data?.data ?? new Uint8Array();
}
serializeBinary(): Uint8Array { /* ... */ }
static deserializeBinary(data: Uint8Array): SimpleMessageImpl { /* ... */ }
toObject(): SimpleMessage { /* ... */ }
}service StreamingService {
rpc GetData(GetDataRequest) returns (stream GetDataResponse);
rpc UploadData(stream UploadDataRequest) returns (UploadDataResponse);
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}Generated TypeScript:
export class StreamingServiceClient {
constructor(private client: GrpcClient) {}
// Server streaming
async* getData(request: GetDataRequestImpl): AsyncGenerator<GetDataResponseImpl> {
const stream = this.client.serverStreamCall(
'test.streaming.StreamingService/GetData',
request.serializeBinary()
);
for await (const response of stream) {
if (response.done) {
break;
}
if (response.data) {
yield GetDataResponseImpl.deserializeBinary(response.data as Uint8Array);
}
}
}
// Client streaming - accepts AsyncIterable
async uploadData(requests: AsyncIterable<UploadDataRequestImpl>): Promise<UploadDataResponseImpl> {
async function* serialize() {
for await (const req of requests) {
yield req.serializeBinary();
}
}
const response = await this.client.clientStreamCall(
'test.streaming.StreamingService/UploadData',
serialize()
);
return UploadDataResponseImpl.deserializeBinary(response.data as Uint8Array);
}
// Bidirectional streaming
async* chat(requests: AsyncIterable<ChatMessageImpl>): AsyncGenerator<ChatMessageImpl> {
async function* serialize() {
for await (const req of requests) {
yield req.serializeBinary();
}
}
const stream = this.client.bidirectionalStreamCall(
'test.streaming.StreamingService/Chat',
serialize()
);
for await (const response of stream) {
if (response.done) {
break;
}
if (response.data) {
yield ChatMessageImpl.deserializeBinary(response.data as Uint8Array);
}
}
}
}import { GrpcClient } from 'grpc-react-native';
import { StreamingServiceClient } from './generated/streaming_service_services';
import { UploadDataRequestImpl } from './generated/streaming_service_types';
const client = new GrpcClient({
host: 'localhost',
port: 50051,
useTls: false
});
await client.initialize();
const serviceClient = new StreamingServiceClient(client);
// Client streaming example
async function* generateUploadRequests() {
for (let i = 0; i < 10; i++) {
yield new UploadDataRequestImpl({
chunk: new TextEncoder().encode(`chunk-${i}`)
});
}
}
const response = await serviceClient.uploadData(generateUploadRequests());
console.log('Upload complete:', response);The plugin includes comprehensive test cases covering all features:
# Run all tests (some legacy tests may fail due to file structure changes)
go test -v# Run only the new comprehensive tests that cover all features
go test -v -run "TestStreaming|TestBinary|TestType|TestDefault|TestRepeated|TestMap|TestNested|TestEnum|TestFieldNaming|TestNamespaceGeneration|TestErrorHandling|TestPerformance"# Test streaming methods
go test -v -run "TestStreaming"
# Test binary serialization
go test -v -run "TestBinary"
# Test type mapping
go test -v -run "TestType"
# Test default values
go test -v -run "TestDefault"
# Test repeated fields
go test -v -run "TestRepeated"
# Test map fields
go test -v -run "TestMap"
# Test nested messages
go test -v -run "TestNested"
# Test enum generation
go test -v -run "TestEnum"The test suite covers:
- Streaming Methods: Server, client, and bidirectional streaming
- Binary Serialization: Complete BinaryWriter/BinaryReader functionality
- Type Mapping: All protobuf types (string, number, bigint, Uint8Array, etc.)
- Default Values: Proper initialization of all field types
- Repeated Fields: Array handling and serialization
- Map Fields: Map<K,V> generation and serialization
- Nested Messages: Complex message structures
- Enum Generation: TypeScript enum generation
- Field Naming: PascalCase conversion
- Namespace Generation: Proper namespace handling
- Error Handling: Edge cases and error conditions
- Performance: Large message handling
# Test with simple message
protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./testoutput -I./testdata testdata/simple.proto
# Test with streaming service
protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./testoutput -I./testdata testdata/streaming_service.proto
# Test with complex types
protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./testoutput -I./testdata testdata/basic_types.proto
# Test with NeoFS protobuf definitions
protoc --plugin=./protoc-gen-grpc-react-native --grpc-react-native_out=./testoutput -I./testdata/neofs testdata/neofs/accounting/service.proto# Check generated files
ls -la testoutput/
# Verify TypeScript syntax (if tsc is available)
npx tsc --noEmit testoutput/*.tsgo build -o protoc-gen-grpc-react-nativego test ./...- Modify
main.goto add new generation logic - Add test cases in
testdata/ - Update expected output in
testoutput/ - Run tests to verify changes
This project is part of the NeoFS TypeScript SDK and follows the same license terms.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
- Initial release with TypeScript generation
- Binary protobuf serialization support
- Full streaming method support
- Cross-package import handling
- NeoFS protobuf compatibility