Unified protobuf code generation for Go, TypeScript, C++, and Rust with WASM.
This repository provides the aptre CLI — a single tool that generates protobuf code for four languages without requiring you to install protoc, protoc plugins, or language-specific toolchains. Everything runs via embedded WebAssembly modules using wazero.
- Zero native dependencies — protoc and plugins run as WASM, no installation required
- Multi-language output — generates Go, TypeScript, C++, and Rust from a single command
- Smart caching — only regenerates when source files actually change
- Go-style imports — use familiar import paths in your
.protofiles
| Language | Message Types | RPC Services | Plugin |
|---|---|---|---|
| Go | *.pb.go |
*_srpc.pb.go |
protobuf-go-lite |
| TypeScript | *.pb.ts |
*_srpc.pb.ts |
protobuf-es-lite |
| C++ | *.pb.cc/h |
*_srpc.pb.hpp/cpp |
Built-in protoc + starpc |
| Rust | *.pb.rs |
*_srpc.pb.rs |
prost (WASI) + starpc |
The Rust code generation uses an embedded WASI build of protoc-gen-prost, meaning you don't need Cargo, rustc, or any Rust toolchain installed to generate .pb.rs files. The prost plugin runs entirely within the wazero WebAssembly runtime alongside protoc itself.
This is powered by go-protoc-gen-prost, which embeds a ~600KB WASM binary that implements the full prost protobuf code generator.
Given a simple proto file:
syntax = "proto3";
package echo;
message EchoMsg {
string body = 1;
}The generated echo.pb.rs:
// @generated
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct EchoMsg {
#[prost(string, tag="1")]
pub body: ::prost::alloc::string::String,
}See starpc for a complete example.
# Run directly (no install needed)
go run github.com/aperturerobotics/common/cmd/aptre@latest generate
# Or install globally
go install github.com/aperturerobotics/common/cmd/aptre@latest- Set up your project with Go and optionally TypeScript:
# Initialize Go module
go mod init github.com/yourorg/yourproject- Create a proto file using Go-style imports:
syntax = "proto3";
package example;
// Import .proto files using Go-style import paths
import "github.com/aperturerobotics/controllerbus/controller/controller.proto";
message GetBusInfoResponse {
repeated controller.Info running_controllers = 1;
}- Generate code:
# Stage files for git (required for discovery)
git add -A
# Generate all languages
go run github.com/aperturerobotics/common/cmd/aptre@latest generate
# Generate only Go output
go run github.com/aperturerobotics/common/cmd/aptre@latest generate --language go
# Or with verbose output
go run github.com/aperturerobotics/common/cmd/aptre@latest generate --verbose| Command | Description |
|---|---|
generate |
Generate protobuf code (Go, TypeScript, C++, Rust) |
generate --force |
Regenerate all files, ignoring cache |
clean |
Remove generated files and cache |
deps |
Ensure all dependencies are installed |
lint |
Run golangci-lint |
fix |
Run golangci-lint with --fix |
test |
Run go test |
test --browser |
Run tests in browser with WebAssembly |
format |
Format Go code with gofumpt |
outdated |
Show outdated dependencies |
The aptre tool orchestrates code generation using embedded WebAssembly:
- Discovery — Finds
.protofiles matching your targets (default:./*.proto) - Caching — Checks
.protoc-manifest.jsonto skip unchanged files - Protoc (WASM) — Runs go-protoc-wasi to parse protos and invoke plugins
- Plugins — Native plugins for Go/TS, WASM plugin for Rust (prost)
- Post-processing — Fixes imports and formats output
┌─────────────────────────────────────────────────────────────┐
│ aptre CLI │
├─────────────────────────────────────────────────────────────┤
│ wazero runtime │
├─────────────┬─────────────────────────┬─────────────────────┤
│ protoc.wasm │ protoc-gen-prost.wasm │ Native Plugins │
│ (parsing) │ (Rust output) │ (Go, TS, C++) │
└─────────────┴─────────────────────────┴─────────────────────┘
C++ protobuf files (.pb.cc and .pb.h) are generated alongside other outputs. Add vendor/ to your include path:
# CMakeLists.txt
include_directories(${PROJECT_SOURCE_DIR}/vendor)#include "github.com/yourorg/yourproject/example/example.pb.h"For StarPC C++ services, the *_srpc.pb.hpp files provide client/server stubs.
The generator uses sensible defaults but can be customized:
- Targets: Proto file patterns (default:
./*.proto) - Exclude: Patterns to skip (e.g.,
vendor/**) - ToolsDir: Plugin binary location (default:
.tools) - Cache: Manifest file (default:
.protoc-manifest.json)
When a repo has a package.json, aptre generate also reads an optional
top-level aptre config object from it.
Example:
{
"name": "spacewave",
"private": true,
"aptre": {
"languages": ["go", "ts", "cpp", "rust"],
"tsImportBoundaries": ["auth", "bldr", "db", "forge", "identity", "net"]
}
}languages selects which output languages to generate. Supported values are
go, ts, cpp, rust, csharp, and python. C# and Python are opt-in.
Omit languages to preserve the existing default: generate Go, C++, and Rust
when the project has a go.mod, plus TypeScript when it has a package.json.
The CLI flag takes precedence over package.json:
aptre generate --language goSetting aptre.rust makes aptre generate compile the whole schema graph in a
single run and write one Rust file per schema, beside the schema, even when a
protobuf package spans several directories. Every other selected language still
uses the per-directory generator.
{
"aptre": {
"module": "example.com/app",
"languages": ["rust"],
"rust": {
"prostOptions": ["enable_type_names", "btree_map=."],
"exclude": [],
"descriptorSet": "wire/descriptors.bin",
"moduleFile": "wire/messages.rs",
"inventory": ".protoc-rust-files.txt"
}
}
}moduleis the import path that prefixes the project's schemas. It defaults to thego.modmodule, so a project withoutgo.modmust set it. Schemas undervendor/keep the import path they have inside that directory.prostOptionsare passed to the prost plugin in order. Use them for type names, deterministic maps, boxed fields andextern_pathmappings.excludelists schema patterns to leave out of compilation.descriptorSetreceives the serialized descriptors of the whole graph, including imports and source information, for runtime reflection.moduleFilereceivesinclude!declarations that nest each generated file under its protobuf package, naming each segment as Prost does (snake case,r#for keywords).inventorylists every generated path. The next run removes files that the inventory names but the schemas no longer produce.
Select schemas with --targets. Outputs are built in memory first and written
only when their bytes change. aptre generate --check --language rust compares
all outputs without writing, prints each outdated: path and exits non-zero.
The check never prepares dependencies: when rpc includes starpc and
protoc-gen-starpc-rust is not built, it fails and names the missing plugin;
run aptre deps first.
tests/rust-graph/check.bash generates a project with every kind of type path,
checks it, and runs the Rust tests over exactly that output.
tsImportBoundaries configures how generated TypeScript protobuf imports are
rewritten inside a monorepo.
By default, same-module generated imports stay relative. That is usually what you want in a single-package repo.
For monorepos that generate protobuf TypeScript into multiple top-level members,
relative imports can break when a build system mirrors sources into another tree
or when each member needs a stable repo-root import path. In that case, define
module-relative boundary prefixes in aptre.tsImportBoundaries.
When a generated *.pb.ts file imports another generated protobuf file:
- If the import stays within the same configured boundary, it remains relative.
- If the import crosses from one configured boundary to another,
aptrerewrites it to an@go/...import. - Imports that resolve outside the current module are still rewritten to
@go/...fromvendor/as before.
Given:
{
"aptre": {
"tsImportBoundaries": ["bldr", "db"]
}
}an import like:
import { VolumeInfo } from '../../db/volume/volume.pb.js'in bldr/plugin/plugin.pb.ts becomes:
import { VolumeInfo } from '@go/github.com/yourorg/yourrepo/db/volume/volume.pb.js'Boundary entries are matched against paths relative to the Go module root, so
they should name repo directories such as bldr, db, or net, not full
module paths.
- starpc — Streaming RPC for Go, TypeScript, and Rust
- protobuf-go-lite — Lightweight Go protobuf without reflection
- protobuf-es-lite — Lightweight TypeScript protobuf
- go-protoc-wasi — Embedded protoc as WASM
- go-protoc-gen-prost — Prost plugin as WASM
- template — Project template using this package
- protobuf-project — More extensive example
Please open a GitHub issue with any questions or issues.
... or reach out on Matrix Chat or Discord.
MIT