# @snowluma/proton Compile-time protobuf encoder/decoder generator for SnowLuma. TypeScript interfaces in, fully-inlined zero-runtime codec functions out. > Vendored from [SnowLuma/protobuf-dsl](https://github.com/SnowLuma/protobuf-dsl) > with edge-case fixes applied. ## What it does You declare your wire schema as plain TypeScript interfaces marked up with `pb`. At build time a Vite plugin walks the call sites of `protobuf_encode` / `protobuf_decode` and replaces them with type-specific, fully-inlined codec functions. No runtime schema lookup, no reflection, no `.proto` files, no `protoc`. ```ts import type { pb, pb_repeated, uint_32, bool } from '@snowluma/proton'; import { protobuf_encode, protobuf_decode } from '@snowluma/proton'; interface UserProfile { id?: pb<1, uint_32>; username?: pb<2, string>; active?: pb<3, bool>; tags?: pb_repeated<4, string>; } const bytes = protobuf_encode({ id: 42, username: 'alice', active: true, tags: ['admin'], }); const user = protobuf_decode(bytes); ``` After the plugin transform, this becomes: ```js // pre-computed tag literals, inlined varint loops, no function-call overhead const bytes = protobuf_encode_UserProfile({ id: 42, ... }); const user = protobuf_decode_UserProfile(bytes); ``` If the plugin **isn't** installed, calling `protobuf_encode` / `protobuf_decode` throws a loud error rather than silently producing wrong bytes. ## Setup in SnowLuma The package is already wired into `@snowluma/core`'s `vitest.config.ts`. To use it elsewhere in the monorepo: ```ts // vite.config.ts (or vitest.config.ts) import { defineConfig } from 'vite'; import protobufVitePlugin from '@snowluma/proton/vite'; export default defineConfig({ plugins: [protobufVitePlugin()], }); ``` Add the workspace dep: ```json { "dependencies": { "@snowluma/proton": "workspace:*" } } ``` ## Primitive types The marker is `pb` (or `pb_repeated` for arrays). Available primitives: | Marker | TS type | Wire | | ----------- | ------------ | ---------------- | | `bool` | `boolean` | varint | | `uint_32` | `number` | varint | | `int_32` | `number` | varint | | `sint_32` | `number` | varint (zigzag) | | `uint_64` | `bigint` | varint | | `int_64` | `bigint` | varint | | `sint_64` | `bigint` | varint (zigzag) | | `fixed_32` | `number` | 32-bit | | `fixed_64` | `bigint` | 64-bit | | `sfixed_32` | `number` | 32-bit | | `sfixed_64` | `bigint` | 64-bit | | `float` | `number` | 32-bit | | `double` | `number` | 64-bit | | `string` | `string` | length-delimited | | `bytes` | `Uint8Array` | length-delimited | Plus message types (any other interface marked with `pb<>` fields) used as the second type-arg to `pb<>` / `pb_repeated<>`. ## Generic templates You can declare reusable wrappers as generic interfaces: ```ts interface Wrapper { value?: pb<1, T>; } const bytes = protobuf_encode>({ value: 42 }); // ^ instantiated at the call site ``` Nested instantiations work too — `Wrapper>` mono-morphizes to `Wrapper__string` + `Wrapper__Wrapper__string` and both end up in the registry at build time. Compose them in concrete interfaces: ```ts interface Container { wrapped?: pb<5, Wrapper>; } ``` …or in other generic templates: ```ts interface Outer { wrapped?: pb<5, Wrapper>; } const bytes = protobuf_encode>({ wrapped: { value: 42 } }); ``` Both forms are covered by regression tests. ## Cross-file & wrapper-binding Schemas can live in one file and call sites in another: ```ts // schema/user.ts export interface UserProfile { id?: pb<1, uint_32>; } // usage.ts import type { UserProfile } from './schema/user'; import { protobuf_encode } from '@snowluma/proton'; const bytes = protobuf_encode({ id: 42 }); ``` You can also build thin generic wrappers around `protobuf_encode` / `_decode` and the plugin will trace through them, including across file boundaries: ```ts // schema/wrap.ts export function encodeWrapped(v: Wrapper): Uint8Array { return protobuf_encode>(v); } // usage.ts import { encodeWrapped } from './schema/wrap'; const bytes = encodeWrapped({ value: 'hi' }); // ^ plugin rewrites to protobuf_encode_Wrapper__string(...) ``` Chained forwarders also work (`A` forwards to `B` which is imported from another module, etc.). ## proto3 default-value semantics **Important behavioural detail.** Proton follows the proto3 wire format spec: fields whose value equals the type's default (`0` / `false` / `""` / empty bytes) are **not emitted** on the wire. This is byte-different from SnowLuma's legacy `protoEncode` runtime, which always emits every field. Both forms are wire-compatible with any conforming proto3 reader (missing fields decode as the default value). When migrating legacy schemas to proton, do **not** treat byte-level inequality on default values as a regression — see [`packages/core/tests/proton-parity.test.ts`](../../packages/core/tests/proton-parity.test.ts) for the test that pins this divergence. If you have a wire consumer that depends on byte-level identity (e.g. for hashing or signing), prefer to set non-default values explicitly rather than relying on the encoder. ## Diagnostics Anything that doesn't resolve to a primitive or a registered message **throws at build time** rather than silently leaking a wrong wire type: ``` Cannot resolve protobuf field type "Partial__UserProfile" on message "Foo" (field "bar", field number 3). The analyzer did not produce a primitive or registered message for this type. Common causes: union / intersection / mapped / conditional types, TypeScript utility types (Partial, Pick, …), qualified names (ns.Type), or a missing import. ``` This is the safety net for TypeScript constructs the analyzer doesn't model. Currently unsupported: - Union / intersection types (`A | B`, `A & B`) - Conditional / mapped types (`T extends X ? A : B`, `{ [K in keyof T]: ... }`) - TS utility types (`Partial`, `Pick`, `Readonly`, …) - Qualified type names (`ns.Type`) - Wrapper functions reached via namespace import (`import * as ns from`) …all of these will throw with a clear message instead of producing bad bytes. ## Optional: runtime-map fallback If a call site can't be statically resolved (e.g. dynamic loading after build), the plugin can emit a JSON map that the runtime can consume to generate codecs on the fly. Off by default. ```ts import protobufVitePlugin from '@snowluma/proton/vite'; export default defineConfig({ plugins: [protobufVitePlugin({ runtimeMap: { enabled: true, fileName: 'snowluma-proton.runtime-map.json' }, })], }); ``` ```ts import runtimeMap from './snowluma-proton.runtime-map.json'; import { protobuf_enableRuntimeMapFallback } from '@snowluma/proton'; protobuf_enableRuntimeMapFallback(runtimeMap); ``` ## Edge cases fixed on top of upstream Each has a regression test in `test/__tests__/analyzer.test.ts`: | # | What used to break | | - | --------------------- | | 1 | `import { pb as P }` — aliased markers were ignored; fields dropped silently | | 2 | `interface Foo { x: pb<1, Wrapper> }` — typeName stuck at `"Wrapper"`, no mono of the inner instantiation | | 3 | `interface Outer { x: pb<1, Wrapper> }` — outer mono didn't re-instantiate the inner generic | | 4 | Wrapper-binding synthetic SourceFile had empty text → `getText()` returned garbage | | 5 | Cross-file forwarded wrappers (chain → imported base) not detected | | 6 | `matchForwardedKnownWrapper` didn't propagate `typePattern` — chain treated `` as the encoded type instead of `Wrapper` | | 7 | Synthetic-SF WeakMap was private to analyzer.ts → replacer used the wrong SF | Plus the build-time guard (above) that turns future analyzer gaps into hard errors instead of silent miscodes. ## Scripts ``` pnpm --filter @snowluma/proton build # bundle src/index.ts → dist/index.js pnpm --filter @snowluma/proton test # vitest run (95 tests) pnpm --filter @snowluma/proton typecheck # tsc --noEmit ``` ## Layout ``` src/ ast/ analyzer + type-tracking pipeline analyzer.ts single-walk message + call-site collection collector.ts interface → ProtobufField/GenericFieldTemplate monomorphizer.ts generic instantiation → concrete ProtobufMessage import-resolver.ts cross-file definition + wrapper resolution callsite.ts matches protobuf_encode/decode invocations utils.ts name resolution + synthetic-SF tracking dependency-graph.ts topo sort + reachability types.ts shared schema types codegen/ inlined wire-format emitters transform/ Vite plugin string edits index.ts plugin entry (./vite) runtime.ts runtime stubs + map fallback (.) runtime-map.ts serialized map format protobuf.d.ts public types (pb<>, pb_repeated<>, primitive aliases) test/ vitest suite (analyzer + cross-file + plugin) ```