SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

@@ -0,0 +1,242 @@
import { describe, expect, it } from 'vitest';
import { run, imports } from '../upstream/compile';
import { bundle } from '../upstream/project';
import { schemaCodecs, schemaRegistry } from '../upstream/schema';
import { generateCode } from '../../src/codegen/generator';
import { protobuf_getUnknownFields } from '../../src/runtime';
import { ProtobufCompiler } from '../../src/compiler/compiler';
import { withProject } from '../upstream/project';
import { join } from 'node:path';
describe('SnowLuma compatibility', () => {
it('leaves generic data unions and nested business types outside codec discovery', () => {
expect(
run(`
type Json = string | Json[];
interface Field<T> {value:T; metadata:Json;}
type Wrapper<T> = Field<T> & { extra?:Json };
function identity<T>(value:Field<T>) {return value;}
function generic<T>(field:Field<Wrapper<T>>) {return identity(field);}
function result<T>(ok:boolean, value:T) {if(ok) return {ok:true, value}; return {ok:false, value};}
const field:Field<Wrapper<string>> = {value:{value:'ok', metadata:[]}, metadata:[]};
export const output = result(true, generic(field)).value.value.value;
`).output,
).toBe('ok');
});
it('keeps unrelated callback aliases and inferred containers lazy', () => {
expect(
run(`
type Json = string | Json[];
type Handler<T> = (value:T) => void;
interface Field<T> {value:T; metadata:Json;}
const field:Field<Json> = {value:'ok', metadata:[]};
function wrap<T>(value:Field<T>) {return {get(){return value.value;}};}
const handler:Handler<Json> = (value) => {};
handler('x');
export const result = wrap(field).get();
`).result,
).toBe('ok');
});
it('propagates composite class bindings through generic factories', () => {
expect(
run(`
interface Box<T> { value:pb<1,T>; }
class Codec<T> { encode(value:T) { return protobuf_encode<T>(value); } }
function create<T>() { return new Codec<Box<T>>(); }
export const result = [...create<uint_32>().encode({value:7})];
`).result,
).toEqual([8, 7]);
});
it('defers unrelated class type arguments but validates codec class schemas', () => {
expect(
run(`
type Json = string | Json[];
class Field<T> { value(): null { return null; } }
function field() { return new Field<Json>(); }
export const result = field().value();
`).result,
).toBeNull();
expect(() =>
run(`
class Codec<T> { encode(value:T) {return protobuf_encode<T>(value);} }
new Codec<Missing>().encode({});
`),
).toThrow(/Unresolved type Missing/);
});
it('infers matching generic containers without expanding unrelated recursive data', () => {
expect(
run(`
type Json = string | Json[];
interface Field<T> { value: T; metadata: Json; }
function arrayField<E>(field: Field<E>) { return Object.assign({}, { get: () => field.value }); }
function wrap<E>(field: Field<E>) { return arrayField(field); }
const field: Field<string> = { value:'ok', metadata:[] };
export const result = wrap(field).get();
`).result,
).toBe('ok');
});
it('does not resolve recursive data aliases while looking for class receivers', () => {
expect(
run(`
type Data<T> = { children: T[]; name: string };
type Element = Data<Element>;
type Wrapped<T> = T;
function data(): Wrapped<Element> { return { children: [], name: 'ok' }; }
export const result = data().name.toUpperCase();
`).result,
).toBe('OK');
});
it('does not resolve ordinary generic return values or data alias members as callbacks', () => {
expect(
run(`
type Id<T> = T;
interface Data { value: string; children: Id<Data>[]; }
const data: Id<Data> = {value:'ok', children:[]};
function decode<T>(raw: Uint8Array, callback: () => T): T | null { return callback(); }
const raw: Uint8Array = new Uint8Array();
const decoded = decode(raw, () => data);
export const result = [decoded.value, data.value];
`).result,
).toEqual(['ok', 'ok']);
});
it('does not specialize ambient library functions', async () => {
const source = `import { expect } from './external';
const data: Record<string, unknown> = {};
expect(data).check();`;
await withProject(
{
'external.d.ts': 'export declare function expect<T>(value: T): { check(): void };',
'entry.ts': source,
},
(directory) => {
expect(
new ProtobufCompiler().compile(source, join(directory, 'entry.ts')),
).toBeNull();
},
);
});
it('leaves ordinary calls and data receivers outside schema resolution', () => {
expect(
run(`
interface Options { meta?: Record<string, unknown>; }
function options(value: Options): Options { return value; }
const input: Options = { meta: { source: 'test' } };
const copy = options(input);
function data(flag: boolean) { if (flag) return { value: 7 }; return { value: 8 }; }
function bytes(value: Uint8Array): Uint8Array { return value; }
const raw: Uint8Array = new Uint8Array([1]);
export const result = [copy.meta.source, data(true).value, bytes(raw).length];
`).result,
).toEqual(['test', 7, 1]);
});
it('keeps TypeScript optionality separate from explicit wire presence', () => {
const result = run(`
interface Msg {
plain?: pb<1, uint_32>;
flag?: pb<2, bool>;
text?: pb<3, string>;
data?: pb<4, bytes>;
big?: pb<5, uint_64>;
forced?: pb_optional<6, uint_32>;
}
export const wire = [...protobuf_encode<Msg>({
plain: 0, flag: false, text: '', data: new Uint8Array(), big: 0n, forced: 0,
})];
export const decoded = protobuf_decode<Msg>(new Uint8Array(wire));
`);
expect(result.wire).toEqual([48, 0]);
expect(result.decoded).toEqual({
plain: null,
flag: null,
text: null,
data: null,
big: null,
forced: 0,
});
});
it('retains aliased markers and presence through exported generic wrappers', async () => {
const result = await bundle({
'schema.ts': `import type { pb as P, pb_optional as O, pb_repeated as R, uint_32 } from '@snowluma/proton';
export interface Box<T> { value?: P<1, T>; }
export interface Msg { plain?: P<1, uint_32>; forced?: O<2, uint_32>; items?: R<3, uint_32>; }
export interface Outer<U> { inner?: P<1, Box<U>>; }`,
'barrel.ts': `export * from './schema';`,
'wrapper.ts': `${imports} import type {Outer} from './barrel';
export function encode<T>(value:Outer<T>) { return protobuf_encode<Outer<T>>(value); }`,
'forward.ts': `import {encode} from './wrapper'; import type {Outer} from './barrel';
export function forward<T>(value:Outer<T>) { return encode<T>(value); }`,
'entry.ts': `${imports} import type {Msg} from './barrel'; import {forward} from './forward';
export const wire = [...forward<Msg>({inner:{value:{plain:0,forced:0,items:[0,7]}}})];`,
});
expect(result.wire).toEqual([10, 8, 10, 6, 16, 0, 24, 0, 24, 7]);
});
it.each([true, false])('resolves package exports with tsconfig=%s', async (withConfig) => {
const result = await bundle({
...(withConfig
? {
'tsconfig.json': JSON.stringify({
compilerOptions: { moduleResolution: 'bundler', module: 'esnext' },
}),
}
: {}),
'node_modules/@fake/schemas/package.json': JSON.stringify({
name: '@fake/schemas',
type: 'module',
exports: { '.': { types: './index.ts', default: './index.ts' } },
}),
'node_modules/@fake/schemas/index.ts': `export * from './schema';`,
'node_modules/@fake/schemas/schema.ts': `export interface Msg { value:pb<1,uint_32>; }`,
'entry.ts': `${imports} import type {Msg} from '@fake/schemas';
export const result = protobuf_decode<Msg>(protobuf_encode<Msg>({value:42}));`,
});
expect(result).toEqual({ result: { value: 42 } });
});
const codec = schemaCodecs<{ value: number }>('interface Msg { value:pb<1,uint_32>; }');
it.each([
['unterminated group', [0x13, 8, 7]],
['mismatched group', [0x13, 0x1c]],
['unexpected group end', [0x14]],
['invalid group tag', [0x13, 0]],
['group payload exceeds frame', [0x13, 0x1a, 4, 0x14]],
['group recursion limit', [...Array(66).fill(0x13), ...Array(66).fill(0x14)]],
] as const)('rejects %s', (_name, bytes) => {
expect(() => codec.decode(Uint8Array.from(bytes))).toThrow(/protobuf/);
});
it('skips nested groups and retains non-enumerable metadata', () => {
const decoded = codec.decode(Uint8Array.from([0x13, 0x1b, 8, 99, 0x1c, 0x14, 8, 7]));
expect(decoded).toEqual({ value: 7 });
expect(protobuf_getUnknownFields(decoded)).toEqual([
{ fieldNumber: 2, wireType: 3, count: 1, totalByteLength: 5 },
]);
expect(JSON.stringify(decoded)).toBe('{"value":7}');
expect([...codec.encode(decoded)]).toEqual([8, 7]);
});
it.each([
[NaN, 0],
[0, NaN],
[0.5, 1],
[0, 0.5],
[-1, 1],
[1, 0],
[0, 3],
[0, Infinity],
])('rejects invalid decoder bounds %s..%s', (offset, end) => {
const generated = generateCode(schemaRegistry('interface Msg { value:pb<1,uint_32>; }'));
const decode = new Function(generated + '\nreturn protobuf_decode_Msg;')();
expect(() => decode(new Uint8Array([8, 1]), offset, end)).toThrow(/bounds/);
});
});
@@ -0,0 +1,342 @@
import { describe, it, expect } from 'vitest';
import { schemaRegistry as analyzeSource } from '../upstream/schema';
import { generateCode } from '../../src/codegen/generator';
import { protobuf_getUnknownFieldMetadata } from '../../src/runtime';
import { loadFixture } from '../helpers';
/** Analyze fixture → generate → eval → return encode/decode pair */
function makeRoundTrip(fixtureName: string, msgName: string) {
return makeRoundTripFromSchema(loadFixture(fixtureName), msgName);
}
function makeRoundTripFromSchema(schema: string, msgName: string) {
const gen = generateCode(analyzeSource(schema, `${msgName}.ts`));
return new Function(
gen + `\nreturn { enc: protobuf_encode_${msgName}, dec: protobuf_decode_${msgName} };`,
)() as {
enc: (obj: unknown) => Uint8Array;
dec: (data: Uint8Array) => any;
};
}
function encodeVarint32(value: number): number[] {
const bytes: number[] = [];
let remaining = value >>> 0;
do {
let byte = remaining & 0x7f;
remaining >>>= 7;
if (remaining !== 0) {
byte |= 0x80;
}
bytes.push(byte);
} while (remaining !== 0);
return bytes;
}
describe('code generator', () => {
it.each([
[0, []],
[1, [0x08, 0x01]],
[127, [0x08, 0x7f]],
[128, [0x08, 0x80, 0x01]],
[300, [0x08, 0xac, 0x02]],
[16384, [0x08, 0x80, 0x80, 0x01]],
[0xffffffff, [0x08, 0xff, 0xff, 0xff, 0xff, 0x0f]],
] as const)('encodes and decodes uint32 wire fixture %i', (value, bytes) => {
const { enc, dec } = makeRoundTrip('simple.ts', 'SimpleMsg');
expect([...enc({ value })]).toEqual(bytes);
expect(dec(Uint8Array.from(bytes)).value).toBe(value);
});
it.each([
[0x10, 0xac, 0x02],
[0x12, 0x03, 0x41, 0x42, 0x43],
[0x15, 0, 0, 0, 0],
[0x11, 0, 0, 0, 0, 0, 0, 0, 0],
])('skips an unknown wire field before a known field (%j)', (...bytes) => {
const { dec } = makeRoundTrip('simple.ts', 'SimpleMsg');
expect(dec(Uint8Array.from([...bytes, 0x08, 42])).value).toBe(42);
});
it('generates encode/decode for simple uint_32', () => {
const gen = generateCode(analyzeSource(loadFixture('simple.ts'), 't.ts'));
expect(gen).toContain('function protobuf_encode_SimpleMsg(obj)');
expect(gen).toContain('function protobuf_decode_SimpleMsg(data');
// Tag for field 1, varint should be pre-computed as 0x08
expect(gen).toContain('buf[offset++] = 8;');
});
it('generates nested message code', () => {
const gen = generateCode(analyzeSource(loadFixture('nested.ts'), 't.ts'));
expect(gen).toContain('__write_Inner(');
expect(gen).toContain('protobuf_decode_Inner(data, offset, offset + _len)');
});
it('round-trip uint_32', () => {
const { enc, dec } = makeRoundTrip('simple.ts', 'SimpleMsg');
expect(dec(enc({ value: 42 })).value).toBe(42);
});
it('round-trip nested message', () => {
const { enc, dec } = makeRoundTrip('nested.ts', 'Outer');
expect(dec(enc({ inner: { value: 999 } })).inner.value).toBe(999);
});
it('round-trip string', () => {
const { enc, dec } = makeRoundTrip('string-msg.ts', 'StringMsg');
expect(dec(enc({ text: 'hello world' })).text).toBe('hello world');
});
it('round-trip unicode string', () => {
const { enc, dec } = makeRoundTrip('string-msg.ts', 'StringMsg');
expect(dec(enc({ text: '你好,protobuf🚀' })).text).toBe('你好,protobuf🚀');
});
it('round-trip bool', () => {
const { enc, dec } = makeRoundTrip('bool-msg.ts', 'BoolMsg');
expect(dec(enc({ active: true })).active).toBe(true);
const empty = enc({ active: false });
expect(empty.length).toBe(0);
expect(dec(empty).active).toBe(false);
});
it('proto3 default encoding (0 → empty)', () => {
const { enc } = makeRoundTrip('simple.ts', 'SimpleMsg');
expect(enc({ value: 0 }).length).toBe(0);
});
it('round-trip repeated uint_32', () => {
const { enc, dec } = makeRoundTrip('repeated.ts', 'RepeatedMsg');
const result = dec(enc({ ids: [10, 20, 30], names: [] }));
expect(result.ids).toEqual([10, 20, 30]);
expect(result.names).toEqual([]);
});
it('round-trip repeated string', () => {
const { enc, dec } = makeRoundTrip('repeated.ts', 'RepeatedMsg');
const result = dec(enc({ ids: [], names: ['hello', 'world'] }));
expect(result.ids).toEqual([]);
expect(result.names).toEqual(['hello', 'world']);
});
it('repeated empty arrays round-trip', () => {
const { enc, dec } = makeRoundTrip('repeated.ts', 'RepeatedMsg');
const result = dec(enc({ ids: [], names: [] }));
expect(result.ids).toEqual([]);
expect(result.names).toEqual([]);
});
it('round-trip bigint-backed 64-bit integers', () => {
const schema = `
interface BigIntMsg {
unsigned: pb<1, uint_64>;
signed: pb<2, int_64>;
zigzag: pb<3, sint_64>;
fixed: pb<4, fixed_64>;
sfixed: pb<5, sfixed_64>;
ids: pb_repeated<6, uint_64>;
}`;
const { enc, dec } = makeRoundTripFromSchema(schema, 'BigIntMsg');
const input = {
unsigned: 18446744073709551615n,
signed: -1n,
zigzag: -1234567890123456789n,
fixed: 0x0102030405060708n,
sfixed: -0x0102030405060708n,
ids: [1n, 2n, 9007199254740993n],
};
expect(dec(enc(input))).toEqual(input);
});
it('round-trip sint_32 with negative values', () => {
const schema = `
interface Sint32Msg {
value: pb<1, sint_32>;
values: pb_repeated<2, sint_32>;
}`;
const { enc, dec } = makeRoundTripFromSchema(schema, 'Sint32Msg');
const input = { value: -789, values: [-1, 0, 1, -789, 2147483647, -2147483648] };
expect(dec(enc(input))).toEqual(input);
});
it('preserves legacy outbound int_32 bytes while accepting canonical negative varints', () => {
const schema = `
interface Int32Msg {
value: pb<1, int_32>;
values: pb_repeated<2, int_32>;
forced: pb_optional<3, int_32>;
}`;
const { enc, dec } = makeRoundTripFromSchema(schema, 'Int32Msg');
const input = {
value: -1,
values: [-2147483648, -1, 0, 1, 2147483647],
forced: -2147483648,
};
const encoded = enc(input);
// Keep SnowLuma's established five-byte negative int32 encoding on the
// outbound path until QQ interoperability is verified on a live account.
// The decoder remains liberal and accepts protobuf's canonical ten-byte
// negative form below.
expect(Array.from(encoded.slice(0, 6))).toEqual([0x08, 0xff, 0xff, 0xff, 0xff, 0x0f]);
expect(dec(encoded)).toEqual(input);
expect(
dec(Uint8Array.from([0x08, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]))
.value,
).toBe(-1);
});
it('retains the low 32 bits of extended uint_32 field varints', () => {
const { dec } = makeRoundTripFromSchema(
`interface Uint32Msg { value: pb<1, uint_32>; }`,
'Uint32Msg',
);
// QQ sometimes serializes a signed 32-bit source into a schema slot that
// clients expose as uint32. Protobuf readers consume the full ten-byte
// varint and retain its low 32 bits for wire compatibility.
expect(
dec(Uint8Array.from([0x08, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]))
.value,
).toBe(0xfffffffe);
// QQ-compatible uint32 readers consume up to a full 64-bit varint and
// retain its low 32 bits. Some push-envelope sequences use that wire
// representation, so values with non-zero high bits must remain decodable.
expect(dec(Uint8Array.from([0x08, 0x80, 0x80, 0x80, 0x80, 0x10])).value).toBe(0);
expect(dec(Uint8Array.from([0x08, 0x87, 0x80, 0x80, 0x80, 0x10])).value).toBe(7);
// Compatibility applies only to complete uint64 varints. Truncated input
// and values wider than 64 bits must still fail loudly.
expect(() => dec(Uint8Array.from([0x08, 0x80]))).toThrow(/[Tt]runcated.*varint/);
expect(() =>
dec(
Uint8Array.from([0x08, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x02]),
),
).toThrow(/Invalid protobuf varint/);
});
it('rejects malformed length-delimited fields without leaving parent bounds', () => {
const schema = `interface BytesMsg { data: pb<1, bytes>; }`;
const { dec } = makeRoundTripFromSchema(schema, 'BytesMsg');
expect(() => dec(Uint8Array.from([0x0a, 0x80]))).toThrow(/[Tt]runcated.*varint/);
expect(() => dec(Uint8Array.from([0x0a, 0x80, 0x80, 0x80, 0x80, 0x10]))).toThrow(
/Invalid protobuf varint/,
);
});
it('skips unknown groups without decoding their fields into the parent', () => {
const schema = `
interface Text { str: pb<1, string>; }
interface Elem { text: pb<1, Text>; }
`;
const { dec } = makeRoundTripFromSchema(schema, 'Elem');
const decoded = dec(
Uint8Array.from([
0x7b, 0x0a, 0x08, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x64, 0x7c,
]),
);
expect(decoded.text).toBeNull();
expect(protobuf_getUnknownFieldMetadata(decoded).fields).toMatchObject([
{ fieldNumber: 15, wireType: 3 },
]);
});
it('aggregates repeated unknown fields instead of retaining every occurrence', () => {
const { dec } = makeRoundTripFromSchema(
`interface UnknownMsg { known: pb<1, uint_32>; }`,
'UnknownMsg',
);
const wire: number[] = [];
const tag = encodeVarint32((60 << 3) | 0);
for (let index = 0; index < 1_000; index++) {
wire.push(...tag, 0);
}
const metadata = protobuf_getUnknownFieldMetadata(dec(Uint8Array.from(wire)));
expect(metadata).toEqual({
fields: [{ fieldNumber: 60, wireType: 0, count: 1_000, totalByteLength: 1_000 }],
totalOccurrences: 1_000,
omittedOccurrences: 0,
omittedByteLength: 0,
});
});
it('caps retained unknown field kinds and summarizes overflow', () => {
const { dec } = makeRoundTripFromSchema(
`interface UnknownKindsMsg { known: pb<1, uint_32>; }`,
'UnknownKindsMsg',
);
const wire: number[] = [];
for (let fieldNumber = 2; fieldNumber <= 66; fieldNumber++) {
wire.push(...encodeVarint32((fieldNumber << 3) | 0), 0);
}
const metadata = protobuf_getUnknownFieldMetadata(dec(Uint8Array.from(wire)));
expect(metadata.fields).toHaveLength(64);
expect(metadata.totalOccurrences).toBe(65);
expect(metadata.omittedOccurrences).toBe(1);
expect(metadata.omittedByteLength).toBe(1);
});
it('round-trip double as ieee754 number', () => {
const schema = `
interface DoubleMsg {
value: pb<1, double>;
values: pb_repeated<2, double>;
}`;
const { enc, dec } = makeRoundTripFromSchema(schema, 'DoubleMsg');
const result = dec(enc({ value: Math.PI, values: [Math.E, -0.5] }));
expect(result.value).toBeCloseTo(Math.PI);
expect(result.values[0]).toBeCloseTo(Math.E);
expect(result.values[1]).toBeCloseTo(-0.5);
});
});
describe('pb_optional explicit presence', () => {
it('serialises a zero scalar (where pb<> would omit it)', () => {
const { enc, dec } = makeRoundTripFromSchema(
`interface OptMsg { forced: pb_optional<1, uint_32>; }`,
'OptMsg',
);
// field 1, varint, value 0 → tag 0x08 then payload 0x00 (NOT empty).
expect(Array.from(enc({ forced: 0 }))).toEqual([0x08, 0x00]);
expect(dec(enc({ forced: 0 })).forced).toBe(0);
});
it('still omits the field when the value is absent (undefined)', () => {
const { enc } = makeRoundTripFromSchema(
`interface OptMsg2 { forced: pb_optional<1, uint_32>; }`,
'OptMsg2',
);
expect(enc({}).length).toBe(0);
});
it('does not change a sibling plain pb<> field (it still omits its zero)', () => {
const { enc, dec } = makeRoundTripFromSchema(
`interface MixMsg { plain: pb<1, uint_32>; forced: pb_optional<2, uint_32>; }`,
'MixMsg',
);
// plain=0 omitted (unchanged); only forced=0 is emitted (field 2 → tag 0x10).
expect(Array.from(enc({ plain: 0, forced: 0 }))).toEqual([0x10, 0x00]);
expect(dec(enc({ plain: 7, forced: 9 }))).toEqual({ plain: 7, forced: 9 });
});
it('forces a false bool onto the wire', () => {
const { enc, dec } = makeRoundTripFromSchema(
`interface BoolOpt { flag: pb_optional<1, bool>; }`,
'BoolOpt',
);
expect(Array.from(enc({ flag: false }))).toEqual([0x08, 0x00]);
expect(dec(enc({ flag: false })).flag).toBe(false);
expect(Array.from(enc({ flag: true }))).toEqual([0x08, 0x01]);
});
});
+3
View File
@@ -0,0 +1,3 @@
interface BoolMsg {
active: pb<1, bool>;
}
+7
View File
@@ -0,0 +1,7 @@
interface Inner {
value: pb<1, uint_32>;
}
interface Outer {
inner: pb<1, Inner>;
}
+9
View File
@@ -0,0 +1,9 @@
interface RepeatedMsg {
ids: pb_repeated<1, uint_32>;
names: pb_repeated<2, string>;
}
const data = protobuf_encode<RepeatedMsg>({ ids: [1, 2, 3], names: ["a", "b"] });
const decoded = protobuf_decode<RepeatedMsg>(data);
export { };
+3
View File
@@ -0,0 +1,3 @@
interface SimpleMsg {
value: pb<1, uint_32>;
}
+3
View File
@@ -0,0 +1,3 @@
interface StringMsg {
text: pb<1, string>;
}
+10
View File
@@ -0,0 +1,10 @@
import { readFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
const fixturesDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'fixtures');
/** Load a fixture file as a string. */
export function loadFixture(name: string): string {
return readFileSync(resolve(fixturesDir, name), 'utf-8');
}
@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest';
import { schemaCodecs } from '../schema';
const bytesSchema = 'interface Msg { data: pb<1,bytes>; }';
describe('encoded buffers', () => {
it('round-trips messages around allocation and slab boundaries', () => {
const codec = schemaCodecs<{ data: Uint8Array }>(bytesSchema);
for (const length of [
0, 1, 61, 62, 63, 64, 65, 125, 127, 128, 4092, 4093, 4094, 4095, 4096, 8192,
]) {
const data = Uint8Array.from({ length }, (_, i) => (i * 13) & 255);
const encoded = codec.encode({ data });
expect(codec.decode(encoded)).toEqual({ data });
if (encoded.length <= 64 || encoded.length > 4096) {
expect(encoded.byteOffset).toBe(0);
expect(encoded.buffer.byteLength).toBe(encoded.length);
}
}
});
it('keeps retained results intact across slab rotation and subsequent mutations', () => {
const codec = schemaCodecs<{ data: Uint8Array }>(bytesSchema);
const retained = Array.from({ length: 200 }, (_, index) => {
const data = new Uint8Array([63, 128, 800, 1300][index % 4]).fill(index);
const encoded = codec.encode({ data });
return { data, encoded, snapshot: encoded.slice() };
});
expect(retained.some(({ encoded }) => encoded.byteOffset > 0)).toBe(true);
for (const { data, encoded, snapshot } of retained) {
expect(encoded).toEqual(snapshot);
expect(codec.decode(encoded)).toEqual({ data });
}
retained[0].encoded.fill(0);
for (const { encoded, snapshot } of retained.slice(1)) {
expect(encoded).toEqual(snapshot);
}
});
it('supports encoding from a getter without reusing the inner result', () => {
const codec = schemaCodecs<{ data: Uint8Array }>(bytesSchema);
const inner = new Uint8Array(128).fill(7);
const outer = new Uint8Array(256).fill(9);
let nested: Uint8Array | undefined;
const encoded = codec.encode({
get data() {
nested = codec.encode({ data: inner });
return outer;
},
});
expect(codec.decode(encoded)).toEqual({ data: outer });
expect(codec.decode(nested!)).toEqual({ data: inner });
});
it('replaces a slab whose backing buffer was transferred', () => {
const codec = schemaCodecs<{ data: Uint8Array }>(bytesSchema);
const data = new Uint8Array(128).fill(3);
const first = codec.encode({ data });
const transferred = structuredClone(first, { transfer: [first.buffer as ArrayBuffer] });
expect(first.byteLength).toBe(0);
expect(codec.decode(transferred)).toEqual({ data });
expect(codec.decode(codec.encode({ data }))).toEqual({ data });
});
it('decodes unaligned pooled messages containing strings, fixed values and nested data', () => {
const codec = schemaCodecs<{
text: string;
ratio: number;
inner: { value: bigint; data: Uint8Array };
}>(`
interface Inner {value:pb<1,fixed_64>;data:pb<2,bytes>;}
interface Msg {text:pb<1,string>;ratio:pb<2,double>;inner:pb<3,Inner>;}
`);
let unaligned = false;
for (let i = 0; i < 100; i++) {
const value = {
text: '中文🚀'.repeat(8) + i,
ratio: Math.PI + i,
inner: { value: (1n << 63n) + BigInt(i), data: new Uint8Array(17 + i).fill(i) },
};
const encoded = codec.encode(value);
unaligned ||= encoded.byteOffset % 8 !== 0;
expect(codec.decode(encoded)).toEqual(value);
}
expect(unaligned).toBe(true);
});
});
@@ -0,0 +1,245 @@
import { describe, it, expect } from 'vitest';
import { schemaRegistry } from '../schema';
import { generateCode } from '../../../src/codegen/generator';
const simple = 'interface SimpleMsg { value: pb<1, uint_32>; }';
const nested =
'interface Inner { value: pb<1, uint_32>; } interface Outer { inner: pb<1, Inner>; }';
const string = 'interface StringMsg { text: pb<1, string>; }';
const bool = 'interface BoolMsg { active: pb<1, bool>; }';
const repeated =
'interface RepeatedMsg { ids: pb_repeated<1, uint_32>; names: pb_repeated<2, string>; }';
function makeRoundTrip(schema: string, msgName: string) {
const gen = generateCode(schemaRegistry(schema, `${msgName}.ts`));
return new Function(
gen + `\nreturn { enc: protobuf_encode_${msgName}, dec: protobuf_decode_${msgName} };`,
)() as {
enc: (obj: any) => Uint8Array;
dec: (data: Uint8Array) => any;
};
}
describe('code generator', () => {
it('allocates one output buffer for nested repeated messages', () => {
const schema = `interface Leaf { text: pb<1,string>; } interface Msg { children: pb_repeated<1,Leaf>; }`;
const registry = schemaRegistry(schema, 'allocation.ts');
const code = generateCode(registry, { encodeRoots: ['Msg'], decodeRoots: [] });
let allocations = 0;
class CountingBytes extends Uint8Array {
constructor(size: number) {
super(size);
allocations++;
}
static get [Symbol.species]() {
return Uint8Array;
}
}
const encode = new Function('Uint8Array', code + '\nreturn protobuf_encode_Msg;')(
CountingBytes,
);
const input = { children: Array.from({ length: 32 }, (_, i) => ({ text: '你好' + i })) };
const encoded = encode(input);
expect(allocations).toBe(1);
expect(makeRoundTrip(schema, 'Msg').dec(encoded)).toEqual(input);
});
it('omits unused helpers and decoder functions from encode-only output', () => {
const code = generateCode(
schemaRegistry('interface Msg { value: pb<1,uint_32>; }', 'minimal.ts'),
{
encodeRoots: ['Msg'],
decodeRoots: [],
},
);
expect(code).not.toContain('TextDecoder');
expect(code).not.toContain('TextEncoder');
expect(code).not.toContain('DataView');
expect(code).not.toContain('BigInt');
expect(code).not.toContain('protobuf_decode_');
});
it('round-trips a deep mixture of strings and nested messages', () => {
const schema = Array.from(
{ length: 24 },
(_, i) =>
`interface M${i} { text: pb<1,string>; ${i < 23 ? `next: pb<2,M${i + 1}>;` : ''} }`,
).join('\n');
let input: any = { text: 'end' };
for (let i = 22; i >= 0; i--) {
input = { text: 'level' + i, next: input };
}
const { enc, dec } = makeRoundTrip(schema, 'M0');
expect(dec(enc(input))).toEqual(input);
});
it('round-trips int_32 and preserves five-byte negative varints', () => {
const { enc, dec } = makeRoundTrip(
`interface IntMsg {
value: pb<1, int_32>;
values: pb_repeated<2, int_32>;
}`,
'IntMsg',
);
expect([...enc({ value: -1 })]).toEqual([0x08, 0xff, 0xff, 0xff, 0xff, 0x0f]);
const input = { value: -2147483648, values: [-1, -2147483648, 0, 2147483647] };
expect(dec(enc(input))).toEqual(input);
});
it('preserves fixed_32 unsigned values and sfixed_32 signed values', () => {
const { enc, dec } = makeRoundTrip(
`interface FixedMsg {
unsigned: pb<1, fixed_32>;
signed: pb<2, sfixed_32>;
values: pb_repeated<3, fixed_32>;
}`,
'FixedMsg',
);
const input = {
unsigned: 0xffffffff,
signed: -2147483648,
values: [0x80000000, 0xffffffff],
};
expect(dec(enc(input))).toEqual(input);
});
it('uses bigint defaults for all 64-bit integer fields', () => {
const { enc, dec } = makeRoundTrip(
`interface Defaults {
unsigned: pb<1, uint_64>;
signed: pb<2, int_64>;
zigzag: pb<3, sint_64>;
fixed: pb<4, fixed_64>;
sfixed: pb<5, sfixed_64>;
}`,
'Defaults',
);
const result = dec(new Uint8Array());
expect(result).toEqual({ unsigned: 0n, signed: 0n, zigzag: 0n, fixed: 0n, sfixed: 0n });
expect(enc(result)).toEqual(new Uint8Array());
});
it('decodes tags with the highest valid field number', () => {
const { enc, dec } = makeRoundTrip(
'interface HighTag { value: pb<536870911, uint_32>; }',
'HighTag',
);
const bytes = new Uint8Array([0xf8, 0xff, 0xff, 0xff, 0x0f, 7]);
expect(enc({ value: 7 })).toEqual(bytes);
expect(dec(bytes)).toEqual({ value: 7 });
});
it('generates encode/decode for simple uint_32', () => {
const gen = generateCode(schemaRegistry(simple, 't.ts'));
expect(gen).toContain('function protobuf_encode_SimpleMsg(obj)');
expect(gen).toContain('function protobuf_decode_SimpleMsg(data');
// Tag for field 1, varint should be pre-computed as 0x08
expect(gen).toContain('buf[offset++] = 8;');
});
it('generates nested message code', () => {
const gen = generateCode(schemaRegistry(nested, 't.ts'));
expect(gen).toContain('protobuf_encode_Inner(');
expect(gen).toContain('protobuf_decode_Inner(data, offset, offset + _len)');
});
it('round-trip uint_32', () => {
const { enc, dec } = makeRoundTrip(simple, 'SimpleMsg');
expect(dec(enc({ value: 42 })).value).toBe(42);
});
it('round-trip nested message', () => {
const { enc, dec } = makeRoundTrip(nested, 'Outer');
expect(dec(enc({ inner: { value: 999 } })).inner.value).toBe(999);
});
it('round-trip string', () => {
const { enc, dec } = makeRoundTrip(string, 'StringMsg');
expect(dec(enc({ text: 'hello world' })).text).toBe('hello world');
});
it('round-trip unicode string', () => {
const { enc, dec } = makeRoundTrip(string, 'StringMsg');
expect(dec(enc({ text: '你好,protobuf🚀' })).text).toBe('你好,protobuf🚀');
});
it('round-trip bool', () => {
const { enc, dec } = makeRoundTrip(bool, 'BoolMsg');
expect(dec(enc({ active: true })).active).toBe(true);
const empty = enc({ active: false });
expect(empty.length).toBe(0);
expect(dec(empty).active).toBe(false);
});
it('proto3 default encoding (0 → empty)', () => {
const { enc } = makeRoundTrip(simple, 'SimpleMsg');
expect(enc({ value: 0 }).length).toBe(0);
});
it('round-trip repeated uint_32', () => {
const { enc, dec } = makeRoundTrip(repeated, 'RepeatedMsg');
const result = dec(enc({ ids: [10, 20, 30], names: [] }));
expect(result.ids).toEqual([10, 20, 30]);
expect(result.names).toEqual([]);
});
it('round-trip repeated string', () => {
const { enc, dec } = makeRoundTrip(repeated, 'RepeatedMsg');
const result = dec(enc({ ids: [], names: ['hello', 'world'] }));
expect(result.ids).toEqual([]);
expect(result.names).toEqual(['hello', 'world']);
});
it('repeated empty arrays round-trip', () => {
const { enc, dec } = makeRoundTrip(repeated, 'RepeatedMsg');
const result = dec(enc({ ids: [], names: [] }));
expect(result.ids).toEqual([]);
expect(result.names).toEqual([]);
});
it('round-trip bigint-backed 64-bit integers', () => {
const schema = `
interface BigIntMsg {
unsigned: pb<1, uint_64>;
signed: pb<2, int_64>;
zigzag: pb<3, sint_64>;
fixed: pb<4, fixed_64>;
sfixed: pb<5, sfixed_64>;
ids: pb_repeated<6, uint_64>;
}`;
const { enc, dec } = makeRoundTrip(schema, 'BigIntMsg');
const input = {
unsigned: 18446744073709551615n,
signed: -1n,
zigzag: -1234567890123456789n,
fixed: 0x0102030405060708n,
sfixed: -0x0102030405060708n,
ids: [1n, 2n, 9007199254740993n],
};
expect(dec(enc(input))).toEqual(input);
});
it('round-trip sint_32 with negative values', () => {
const schema = `
interface Sint32Msg {
value: pb<1, sint_32>;
values: pb_repeated<2, sint_32>;
}`;
const { enc, dec } = makeRoundTrip(schema, 'Sint32Msg');
const input = { value: -789, values: [-1, 0, 1, -789, 2147483647, -2147483648] };
expect(dec(enc(input))).toEqual(input);
});
it('round-trip double as ieee754 number', () => {
const schema = `
interface DoubleMsg {
value: pb<1, double>;
values: pb_repeated<2, double>;
}`;
const { enc, dec } = makeRoundTrip(schema, 'DoubleMsg');
const result = dec(enc({ value: Math.PI, values: [Math.E, -0.5] }));
expect(result.value).toBeCloseTo(Math.PI);
expect(result.values[0]).toBeCloseTo(Math.E);
expect(result.values[1]).toBeCloseTo(-0.5);
});
});
@@ -0,0 +1,212 @@
import { deepStrictEqual } from 'node:assert';
import { describe, it, expect } from 'vitest';
import { schemaRegistry } from '../schema';
import { generateCode } from '../../../src/codegen/generator';
const code = generateCode(
schemaRegistry(`
interface Text { value: pb<1,string>; }
interface Nested { texts: pb_repeated<1,Text>; }
`),
);
const { encode, decode, encodeNested, decodeNested } = new Function(`${code}
return {
encode: protobuf_encode_Text, decode: protobuf_decode_Text,
encodeNested: protobuf_encode_Nested, decodeNested: protobuf_decode_Nested,
};`)() as {
decode(bytes: Uint8Array): { value: string };
encode(value: { value: string }): Uint8Array;
encodeNested(value: { texts: { value: string }[] }): Uint8Array;
decodeNested(bytes: Uint8Array): { texts: { value: string }[] };
};
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function wire(payload: Uint8Array): Uint8Array {
const prefix = [10];
let length = payload.length;
do {
prefix.push((length & 127) | (length >= 128 ? 128 : 0));
length >>>= 7;
} while (length);
return Uint8Array.from([...prefix, ...payload]);
}
describe('UTF-8 codecs', () => {
it('matches TextEncoder across native encoding boundaries, including unpaired surrogates', () => {
for (const length of [31, 32, 33, 42, 43, 63, 64, 65, 127, 128, 129, 4096]) {
for (const unit of ['a', 'é', '中文', '🚀', '\ud800', '\udc00', 'a\ud800']) {
const value = unit.repeat(length);
// Generic iterable equality repeats per-byte work on these large views.
deepStrictEqual(encode({ value }), wire(encoder.encode(value)));
}
}
});
it('preserves every ASCII byte across short and native decoding boundaries', () => {
for (const length of [
0, 1, 3, 4, 7, 8, 15, 16, 23, 24, 30, 31, 32, 33, 63, 64, 127, 128, 4096,
]) {
for (let byte = 0; byte < 128; byte++) {
const value = String.fromCharCode(byte).repeat(length);
deepStrictEqual(
encode({ value }),
length ? wire(encoder.encode(value)) : new Uint8Array(),
);
expect(decode(wire(encoder.encode(value)))).toEqual({ value });
}
}
});
it('matches TextDecoder for Unicode after ASCII prefixes and at slice boundaries', () => {
const suffixes = [
'\u0080',
'é',
'\u07ff',
'\u0800',
'中文',
'\ud7ff',
'\ue000',
'\uffff',
'🚀',
'\u{10000}',
'\u{10ffff}',
'\ufeff',
'\ufeffx\ufeff',
'\ud800',
'\udc00',
];
for (let length = 0; length <= 64; length++) {
for (const suffix of suffixes) {
const value = 'a'.repeat(length) + suffix;
const payload = encoder.encode(value);
const bytes = wire(payload);
const padded = Uint8Array.from([255, 255, ...bytes, 128, 128]);
expect(decode(padded.subarray(2, 2 + bytes.length)).value).toBe(
decoder.decode(payload),
);
}
}
});
it('matches replacement behavior for all one-byte and two-byte sequences', () => {
const payload = new Uint8Array(2);
for (let first = 0; first < 256; first++) {
payload[0] = first;
const one = payload.subarray(0, 1);
expect(decode(wire(one)).value).toBe(decoder.decode(one));
for (let second = 0; second < 256; second++) {
payload[1] = second;
expect(decode(wire(payload)).value).toBe(decoder.decode(payload));
}
}
});
it('matches TextDecoder across randomized valid Unicode strings', () => {
let state = 0x5a17cafe;
for (let sample = 0; sample < 5000; sample++) {
let value = 'a'.repeat(sample % 17);
for (let index = 0; index < sample % 21; index++) {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
value += String.fromCodePoint(state % 0x110000);
}
const payload = encoder.encode(value);
expect(decode(wire(payload)).value).toBe(decoder.decode(payload));
}
});
it('matches TextDecoder for invalid and truncated multibyte sequences', () => {
const sequences = [
[0xe0, 0x80, 0x80],
[0xed, 0xa0, 0x80],
[0xf0, 0x80, 0x80, 0x80],
[0xf4, 0x90, 0x80, 0x80],
[0xf5, 0x80, 0x80, 0x80],
[0xff],
[0xc2],
[0xe1, 0x80],
[0xf1, 0x80, 0x80],
[0xe1, 65, 0x80],
[0xf1, 0x80, 65, 0x80],
[0xef, 0xbb, 0xbf, 0xff],
];
for (let length = 0; length <= 33; length++) {
for (const sequence of sequences) {
const payload = Uint8Array.from([...Array(length).fill(65), ...sequence]);
expect(decode(wire(payload)).value).toBe(decoder.decode(payload));
}
}
let state = 0x42abcdef;
for (let sample = 0; sample < 5000; sample++) {
const payload = Uint8Array.from({ length: sample % 65 }, () => {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
return state >>> 24;
});
expect(decode(wire(payload)).value).toBe(decoder.decode(payload));
}
});
it('keeps nested and repeated strings independent', () => {
const texts = [
'',
'ascii',
'a'.repeat(31),
'a'.repeat(32),
'中文🚀',
'a\ufeffb',
'\ufeffx',
'中文🚀'.repeat(64),
].map((value) => ({ value }));
expect(decodeNested(encodeNested({ texts }))).toEqual({
texts: texts.map(({ value }) => ({ value: decoder.decode(encoder.encode(value)) })),
});
// A truncated UTF-8 sequence must not consume bytes from the following field.
const first = wire(Uint8Array.from([0xf0, 0x9f]));
const second = wire(encoder.encode('next'));
const nested = Uint8Array.from([10, first.length, ...first, 10, second.length, ...second]);
expect(decodeNested(nested)).toEqual({ texts: [{ value: '�' }, { value: 'next' }] });
});
it('measures UTF-8 correctly when a code point crosses a scratch-buffer boundary', () => {
for (const length of [4093, 4094, 4095, 4096, 4097, 8191]) {
for (const suffix of ['é', '中', '🚀', '\ud800', '\udc00', '\ud800x']) {
const value = 'a'.repeat(length) + suffix + '中文🚀'.repeat(4000);
deepStrictEqual(encode({ value }), wire(encoder.encode(value)));
}
}
});
it('preserves replacement and BOM behavior after long malformed inputs', () => {
for (const length of [127, 128, 129, 255, 4095, 4096, 4097]) {
for (const sequence of [
[0xff],
[0xe0, 0x80, 0x80],
[0xed, 0xa0, 0x80],
[0xf0, 0x9f],
[0xef, 0xbb, 0xbf],
]) {
for (const prefix of [[], [0xef, 0xbb, 0xbf]]) {
const payload = Uint8Array.from([
...prefix,
...Array(length).fill(65),
...sequence,
]);
expect(decode(wire(payload)).value).toBe(decoder.decode(payload));
const valid = encoder.encode('中文🚀'.repeat(64));
expect(decode(wire(valid)).value).toBe(decoder.decode(valid));
}
}
}
});
});
@@ -0,0 +1,202 @@
import { describe, it, expect } from 'vitest';
import { schemaCodecs as codecs } from '../schema';
describe('generated wire format', () => {
it('matches TextEncoder at UTF-8 length-prefix boundaries', () => {
const codec = codecs<{ value: string }>('interface Msg {value:pb<1,string>;}');
for (const length of [127, 128, 16383, 16384]) {
for (const value of ['a'.repeat(length), '界🚀\ud800'.repeat(length)]) {
const payload = new TextEncoder().encode(value);
const prefix = [10];
let remaining = payload.length;
do {
prefix.push((remaining % 128) | (remaining >= 128 ? 128 : 0));
remaining = Math.floor(remaining / 128);
} while (remaining);
const expected = new Uint8Array(prefix.length + payload.length);
expected.set(prefix);
expected.set(payload, prefix.length);
expect(codec.encode({ value })).toEqual(expected);
expect(codec.decode(expected)).toEqual({
value: new TextDecoder().decode(payload),
});
}
}
}, 30_000);
it('decodes floating-point, fixed-width and byte fields from unaligned subarrays', () => {
const codec = codecs<{ f: number; d: number; fixed: bigint; data: Uint8Array }>(
`interface Msg {f:pb<1,float>;d:pb<2,double>;fixed:pb<3,fixed_64>;data:pb<4,bytes>;}`,
);
const input = {
f: Math.fround(1 / 3),
d: -Number.MAX_VALUE,
fixed: (1n << 64n) - 1n,
data: new Uint8Array([0, 1, 255]),
};
const wire = codec.encode(input);
for (const padding of [1, 3, 7, 31]) {
const buffer = new Uint8Array(wire.length + padding + 5).fill(255);
buffer.set(wire, padding);
expect(codec.decode(buffer.subarray(padding, padding + wire.length))).toEqual(input);
}
});
it('matches an independent bigint varint reference at every byte boundary', () => {
const codec = codecs<{ value: bigint }>('interface Msg { value: pb<1, uint_64>; }');
const values = new Set<bigint>([0n, 1n, (1n << 64n) - 1n]);
for (let bit = 1n; bit < 64n; bit++) {
values.add((1n << bit) - 1n);
values.add(1n << bit);
values.add((1n << bit) + 1n);
}
for (const value of values) {
let rest = value;
const bytes = [];
do {
bytes.push(Number(rest & 127n) | (rest > 127n ? 128 : 0));
rest >>= 7n;
} while (rest);
const wire = new Uint8Array([8, ...bytes]);
expect(codec.encode({ value })).toEqual(value === 0n ? new Uint8Array() : wire);
expect(codec.decode(wire)).toEqual({ value });
}
});
it('decodes nonzero booleans whose low 32 bits are zero', () => {
const codec = codecs<{ value: boolean }>('interface Msg { value: pb<1, bool>; }');
expect(codec.decode(new Uint8Array([8, 128, 128, 128, 128, 16]))).toEqual({ value: true });
});
const uint32 = codecs<{ value: number }>('interface Msg { value: pb<1, uint_32>; }');
const varints: [number, number[]][] = [
[1, [0x01]],
[127, [0x7f]],
[128, [0x80, 0x01]],
[300, [0xac, 0x02]],
[16384, [0x80, 0x80, 0x01]],
[0xffffffff, [0xff, 0xff, 0xff, 0xff, 0x0f]],
];
it.each(varints)('encodes uint32 %i using the expected wire bytes', (value, bytes) => {
expect([...uint32.encode({ value })]).toEqual([0x08, ...bytes]);
});
it.each(varints)('decodes uint32 %i from independent wire bytes', (value, bytes) => {
expect(uint32.decode(new Uint8Array([0x08, ...bytes]))).toEqual({ value });
});
it('omits scalar zero and decodes an explicit zero', () => {
expect(uint32.encode({ value: 0 })).toEqual(new Uint8Array());
expect(uint32.decode(new Uint8Array([8, 0]))).toEqual({ value: 0 });
});
it.each(['hello', '', '你好', '🚀', '\ud800', '\udc00'])('encodes UTF-8 %j', (value) => {
const codec = codecs<{ value: string }>('interface Msg { value: pb<1, string>; }');
const payload = new TextEncoder().encode(value);
const bytes = new Uint8Array([0x0a, payload.length, ...payload]);
expect(codec.encode({ value })).toEqual(value === '' ? new Uint8Array() : bytes);
expect(codec.decode(bytes)).toEqual({ value: new TextDecoder().decode(payload) });
});
it('encodes bytes with a length prefix and copies the decoded data', () => {
const codec = codecs<{ value: Uint8Array }>('interface Msg { value: pb<1, bytes>; }');
const bytes = new Uint8Array([10, 3, 1, 128, 255]);
const value = new Uint8Array([1, 128, 255]);
expect(codec.encode({ value })).toEqual(bytes);
const result = codec.decode(bytes);
bytes[2] = 0;
expect(result.value).toEqual(value);
});
it.each([
['varint', [0x10, 0xac, 0x02]],
['64-bit', [0x11, 1, 2, 3, 4, 5, 6, 7, 8]],
['length-delimited', [0x12, 3, 0x41, 0x42, 0x43]],
['32-bit', [0x15, 1, 2, 3, 4]],
])('skips unknown %s fields and resumes decoding', (_name, bytes) => {
expect(uint32.decode(new Uint8Array([...(bytes as number[]), 8, 7]))).toEqual({ value: 7 });
});
const packedCases: [string, (number | bigint | boolean)[], number[]][] = [
['uint_32', [1, 300], [1, 0xac, 2]],
['int_32', [1, -2], [1, 0xfe, ...Array(8).fill(0xff), 1]],
['sint_32', [-1, 1], [1, 2]],
['bool', [true, false], [1, 0]],
['uint_64', [1n, 300n], [1, 0xac, 2]],
['int_64', [1n, -2n], [1, 0xfe, ...Array(8).fill(0xff), 1]],
['sint_64', [-1n, 1n], [1, 2]],
['fixed_32', [0xffffffff, 1], [255, 255, 255, 255, 1, 0, 0, 0]],
['sfixed_32', [-1, 1], [255, 255, 255, 255, 1, 0, 0, 0]],
['fixed_64', [1n], [1, 0, 0, 0, 0, 0, 0, 0]],
['sfixed_64', [-1n], Array(8).fill(255)],
['float', [1], [0, 0, 128, 63]],
['double', [1], [0, 0, 0, 0, 0, 0, 240, 63]],
];
it.each(packedCases)(
'decodes packed, unpacked and split %s arrays',
(type, values, payload) => {
const codec = codecs<{ values: typeof values }>(
`interface Msg { values: pb_repeated<1, ${type}>; }`,
);
const packed = [10, payload.length, ...payload];
const unpacked = codec.encode({ values });
expect(codec.decode(new Uint8Array(packed))).toEqual({ values });
expect(codec.decode(new Uint8Array([...packed, ...unpacked, ...packed]))).toEqual({
values: [...values, ...values, ...values],
});
expect(codec.decode(new Uint8Array([10, 0]))).toEqual({ values: [] });
},
);
const malformedCodec = codecs(`
interface Inner { value: pb<1, uint_32>; }
interface Msg {
value: pb<1, uint_32>;
big: pb<2, uint_64>;
text: pb<3, string>;
data: pb<4, bytes>;
float: pb<5, float>;
double: pb<6, double>;
inner: pb<7, Inner>;
values: pb_repeated<8, uint_32>;
fixed: pb_repeated<9, fixed_32>;
}
`);
const invalidCases: [string, number[]][] = [
['zero tag', [0]],
['invalid wire type', [15]],
['truncated tag', [128]],
['overflowing tag', [255, 255, 255, 255, 31]],
['missing varint', [8]],
['unterminated varint', [8, 128]],
['overflowing varint', [8, ...Array(10).fill(255)]],
['truncated bigint', [16, 128]],
['overflowing bigint', [16, ...Array(9).fill(255), 2]],
['missing string length', [26]],
['truncated string', [26, 2, 65]],
['overflowing length', [26, 255, 255, 255, 255, 31]],
['truncated bytes', [34, 2, 1]],
['truncated float', [45, 0]],
['truncated double', [49, 0]],
['nested message exceeds parent', [58, 5, 8, 1]],
['nested varint exceeds nested boundary', [58, 2, 8, 128, 8, 7]],
['packed varint exceeds packed boundary', [66, 1, 128, 8, 7]],
['packed fixed32 has partial value', [74, 3, 1, 2, 3, 8, 7]],
['unknown truncated varint', [80, 128]],
['unknown truncated fixed64', [81, 0]],
['unknown truncated bytes', [82, 5, 1]],
['unknown truncated fixed32', [85, 0]],
];
it.each(invalidCases)('rejects %s', (_name, bytes) => {
expect(() => malformedCodec.decode(new Uint8Array(bytes))).toThrow(/protobuf/);
});
});
+24
View File
@@ -0,0 +1,24 @@
import ts from 'typescript';
import { ProtobufCompiler } from '../../src/compiler/compiler';
import { packageName } from '../../src/package';
export const imports = `import { protobuf_encode, protobuf_decode } from '${packageName}';`;
export function run(body: string): any {
const compiler = new ProtobufCompiler();
const source = imports + '\n' + body;
const transformed = compiler.compile(source, 'test-entry.ts') ?? source;
const js = ts.transpileModule(transformed, {
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS },
}).outputText;
const exports = {};
new Function('exports', 'require', js)(exports, () => ({
protobuf_encode() {
throw new Error('Uncompiled encode');
},
protobuf_decode() {
throw new Error('Uncompiled decode');
},
}));
return exports;
}
@@ -0,0 +1,332 @@
import { bundle } from '../project';
import { describe, expect, it } from 'vitest';
import { run, imports } from '../compile';
const schema = `interface Msg { value: pb<1,uint_32>; }
function encode<T>(value:T) { return protobuf_encode<T>(value); }`;
describe('generic callbacks', () => {
it.each([
'const callbacks=[encode];',
'const api={send:encode};',
'[{value:7}].map(<T>(value:T)=>protobuf_encode<T>(value));',
'[{value:7}].map((encode));',
])('rejects callback values without enough schema context: %s', (expression) => {
expect(() => run(schema + expression)).toThrow(/Cannot infer callback schema/);
});
it('supports callbacks in default parameters and codec calls in parameter initializers', () => {
expect(
run(`${schema}
function apply<T>(value:T, callback:(value:T)=>Uint8Array=protobuf_encode) {return callback(value);}
function defaults<T>(value:T, bytes=protobuf_encode<T>(value)) {return bytes;}
export const result=[[...apply<Msg>({value:7})],[...defaults<Msg>({value:8})]];
export const lengths=[apply.length, defaults.length];
`),
).toEqual({
result: [
[8, 7],
[8, 8],
],
lengths: [1, 1],
});
});
it('supports destructured polymorphic callback parameters', () => {
expect(
run(`${schema}
interface Api {encode:<T>(value:T)=>Uint8Array;}
function use<T>({encode:fn}:Api,value:T) {return fn<T>(value);}
export const result=[...use<Msg>({encode},{value:7})];
`).result,
).toEqual([8, 7]);
});
it('propagates object callback signatures through function parameters', () => {
expect(
run(`${schema}
interface Api {encode:<T>(value:T)=>Uint8Array;}
function use<T>(api:Api,value:T) {return api.encode<T>(value);}
export const result=[...use<Msg>({encode},{value:7})];
`).result,
).toEqual([8, 7]);
});
it('adapts typed assignments and callbacks stored in arrays', () => {
expect(
run(`${schema}
type Encoder=(value:Msg)=>Uint8Array;
let callback:Encoder;
callback=encode;
const callbacks:Encoder[]=[encode,callback];
export const result=callbacks.map(fn=>[...fn({value:7})]);
`).result,
).toEqual([
[8, 7],
[8, 7],
]);
});
it('reports opaque polymorphic consumers at build time', () => {
expect(() =>
run(`${schema}
declare function external(fn:<T>(value:T)=>Uint8Array):void;
external(encode);
`),
).toThrow(/receiving implementation/);
});
it('supports class arrow fields as callbacks with lexical this', () => {
expect(
run(`${schema}
class Codec<T> { calls=0; encode=(value:T)=>{this.calls++;return protobuf_encode<T>(value);}; }
const codec=new Codec<Msg>();
export const result=[{value:7}].map(codec.encode).map(bytes=>[...bytes]);
export const calls=codec.calls;
`),
).toEqual({ result: [[8, 7]], calls: 1 });
});
it('preserves spread evaluation, method receivers and rest arguments', () => {
expect(
run(`${schema}
const events:string[]=[];
const api={calls:0,send<T>(value:T,...labels:string[]){this.calls++;events.push(...labels);return protobuf_encode<T>(value);}};
function args():[Msg,string,string] {events.push('args');return [{value:7},'a','b'];}
export const result=[...api.send<Msg>(...args())];
export const calls=api.calls;
export {events};
`),
).toEqual({ result: [8, 7], calls: 1, events: ['args', 'a', 'b'] });
});
it('analyzes shared higher order functions across multiple build entries', async () => {
expect(
await bundle(
{
'apply.ts':
'export function apply<T>(value:T, callback:<U>(value:U)=>Uint8Array) { return callback<T>(value); }',
'plain.ts': `${imports} import {apply} from './apply'; interface Msg {value:pb<1,uint_32>;}
function encode<T>(value:T) {return protobuf_encode<T>(value);}
export const result=[...apply<Msg>({value:7},encode)];`,
'boxed.ts': `${imports} import {apply} from './apply'; interface Msg {value:pb<1,uint_32>;} interface Box<T> {value:pb<1,T>;}
function encode<T>(value:T) {return protobuf_encode<Box<T>>({value});}
export const result=[...apply<Msg>({value:8},encode)];`,
},
['plain.ts', 'boxed.ts'],
),
).toEqual({ plain: { result: [8, 7] }, boxed: { result: [10, 2, 8, 8] } });
});
it('preserves this parameters, bound generic methods and class arguments', () => {
expect(
run(`${schema}
interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
class Codec<T> {
calls=0;
encode<U>(this:Codec<T>, value:Pair<T,U>) { this.calls++; return protobuf_encode<Pair<T,U>>(value); }
}
const codec = new Codec<uint_32>();
const callback = codec.encode<string>.bind(codec);
export const result = [...callback({first:7,second:'x'})];
export const calls = codec.calls;
`),
).toEqual({ result: [8, 7, 18, 1, 120], calls: 1 });
});
it('supports returned polymorphic functions and typed object callbacks', () => {
expect(
run(`${schema}
type Encoder = <T>(value:T)=>Uint8Array;
function factory(): Encoder { return encode; }
const callback = factory();
const api: {encode:Encoder} = {encode};
export const result = [[...callback<Msg>({value:7})], [...api.encode<Msg>({value:8})], [...factory()<Msg>({value:9})]];
`).result,
).toEqual([
[8, 7],
[8, 8],
[8, 9],
]);
});
it('supports nested generic callback aliases', () => {
expect(
run(`${schema}
type Callback<T> = (value:T)=>Uint8Array;
type Alias<T> = Callback<T>;
const callback: Alias<Msg> = encode;
export const result = [...callback({value:7})];
`).result,
).toEqual([8, 7]);
});
it('passes all required codecs when callback implementations differ', () => {
expect(
run(`${schema}
interface Box<T> {value:pb<1,T>;}
function boxed<T>(value:T) { return protobuf_encode<Box<T>>({value}); }
function apply<T>(value:T, callback:<U>(value:U)=>Uint8Array) { return callback<T>(value); }
export const result = [[...apply<Msg>({value:7}, encode)], [...apply<Msg>({value:8}, boxed)]];
`).result,
).toEqual([
[8, 7],
[10, 2, 8, 8],
]);
});
it('supports a callback implementation accepting fewer parameters than its signature', () => {
expect(
run(`${schema}
function apply(callback:<T>(value:T, index:number)=>Uint8Array) { return callback<Msg>({value:7}, 42); }
export const result = [...apply(encode)];
`).result,
).toEqual([8, 7]);
});
it('preserves default parameter evaluation and callback length', () => {
expect(
run(`${schema}
let defaults=0;
function callback<T>(value:T, flag=++defaults) { return flag ? protobuf_encode<T>(value) : new Uint8Array(); }
const cb = callback<Msg>;
export const result = [...cb({value:7})];
export const length = cb.length;
export {defaults};
`),
).toEqual({ result: [8, 7], length: 1, defaults: 1 });
});
it('propagates nested schema substitutions through callback chains', () => {
expect(
run(`${schema}
interface Box<T> {value:pb<1,T>;}
function nested<T>(value:Box<T>) { return protobuf_encode<Box<T>>(value); }
function apply<T>(value:Box<T>, fn:<U>(value:Box<U>)=>Uint8Array) { return fn<T>(value); }
export const result = [...apply<Msg>({value:{value:7}}, nested)];
`).result,
).toEqual([10, 2, 8, 7]);
});
it('preserves async callback bodies', async () => {
const result = run(`${schema}
async function send<T>(value:T) { await Promise.resolve(); return protobuf_encode<T>(value); }
const values: Msg[] = [{value:7},{value:8}];
export const result = Promise.all(values.map(send));
`);
expect((await result.result).map((bytes: Uint8Array) => [...bytes])).toEqual([
[8, 7],
[8, 8],
]);
});
it('materializes instantiation expressions in variables, returns and arrays', () => {
expect(
run(`${schema}
const callback = encode<Msg>;
function factory() { return encode<Msg>; }
const values = [encode<Msg>, factory(), callback];
export const result = values.map(fn => [...fn({value:7})]);
`).result,
).toEqual([
[8, 7],
[8, 7],
[8, 7],
]);
});
it('infers array callbacks and preserves side effects and rest arguments', () => {
expect(
run(`${schema}
const values: Msg[] = [{value:7}, {value:8}];
const calls: number[] = [];
function callback<T>(value:T, ...rest: unknown[]) { calls.push(rest[0] as number); return protobuf_encode<T>(value); }
export const result = values.map(callback).map(bytes => [...bytes]);
export { calls };
`),
).toEqual({
result: [
[8, 7],
[8, 8],
],
calls: [0, 1],
});
});
it('adapts runtime function values directly', () => {
expect(
run(`${schema}
const values: Msg[] = [{value:7}];
const encodeMsg = protobuf_encode<Msg>;
const decodeMsg: (bytes: Uint8Array) => Msg = protobuf_decode;
export const result = [decodeMsg(encodeMsg(values[0])), ...values.map(protobuf_encode).map(decodeMsg)];
`).result,
).toEqual([{ value: 7 }, { value: 7 }]);
});
it('passes codecs through a generic higher order function', () => {
expect(
run(`${schema}
function apply<T>(value:T, callback:(value:T)=>Uint8Array) { return callback(value); }
export const result = [...apply<Msg>({value:7}, encode)];
`).result,
).toEqual([8, 7]);
});
it('supports independently generic callbacks and transitive forwarding', () => {
expect(
run(`${schema}
type Encoder = <T>(value:T)=>Uint8Array;
function invoke<T>(value:T, callback:Encoder) { return callback<T>(value); }
function forward<T>(value:T, callback:Encoder) { return invoke<T>(value, callback); }
export const result = [...forward<Msg>({value:7}, encode)];
`).result,
).toEqual([8, 7]);
});
it('keeps distinct schemas when one generic callback is reused', () => {
expect(
run(`${schema}
interface Text { value: pb<1,string>; }
function both(callback:<T>(value:T)=>Uint8Array) {
return [[...callback<Msg>({value:7})], [...callback<Text>({value:'x'})]];
}
export const result = both(encode);
`).result,
).toEqual([
[8, 7],
[10, 1, 120],
]);
});
it('captures codecs in returned callbacks and contextual aliases', () => {
expect(
run(`${schema}
type Encoder<T> = (value:T)=>Uint8Array;
function factory<T>(): Encoder<T> { return encode; }
const callback: Encoder<Msg> = encode;
export const result = [[...callback({value:7})], [...factory<Msg>()({value:8})]];
`).result,
).toEqual([
[8, 7],
[8, 8],
]);
});
it('supports generic inline callbacks and preserves native thisArg', () => {
expect(
run(`${schema}
const values: Msg[] = [{value:7}];
const state = { calls:0 };
function callback<T>(value:T) { this.calls++; return protobuf_encode<T>(value); }
export const a = values.map(callback, state).map(bytes => [...bytes]);
export const b = values.map(<T>(value:T) => protobuf_encode<T>(value)).map(bytes => [...bytes]);
export const calls = state.calls;
`),
).toEqual({ a: [[8, 7]], b: [[8, 7]], calls: 1 });
});
it('handles imported higher order functions and callback aliases', async () => {
expect(
await bundle({
'encoder.ts': `${imports} export function encode<T>(value:T) { return protobuf_encode<T>(value); }`,
'apply.ts': `export function apply<T>(value:T, fn:<U>(value:U)=>Uint8Array) { return fn<T>(value); }`,
'entry.ts': `import {encode as callback} from './encoder'; import {apply} from './apply'; interface Msg {value:pb<1,uint_32>;} export const result = [...apply<Msg>({value:7}, callback)];`,
}),
).toEqual({ result: [8, 7] });
});
});
@@ -0,0 +1,196 @@
import { bundle } from '../project';
import { describe, expect, it } from 'vitest';
import { run, imports } from '../compile';
import { ProtobufCompiler } from '../../../src/compiler/compiler';
const box = 'interface Box<T> {value:pb<1,T>;}';
const codec = `${box}
class Codec<T> {
encode(value:Box<T>) {return protobuf_encode<Box<T>>(value);}
decode(bytes:Uint8Array) {return protobuf_decode<Box<T>>(bytes);}
}`;
describe('generic class instances', () => {
it('isolates uint32 and string instances across alternating calls', () => {
const result = run(`${codec}
const numbers=new Codec<uint_32>(); const strings=new Codec<string>();
export const result=Array.from({length:32},(_,i)=>[numbers.decode(numbers.encode({value:i})),strings.decode(strings.encode({value:'v'+i}))]);
export const sharedMethod=numbers.encode===strings.encode;
export const keys=Object.keys(numbers);
`);
expect(result.result).toEqual(
Array.from({ length: 32 }, (_, i) => [{ value: i }, { value: 'v' + i }]),
);
expect(result.sharedMethod).toBe(true);
expect(result.keys).toEqual([]);
});
it('emits one codec per concrete type for many instances', () => {
const declarations = Array.from(
{ length: 32 },
(_, i) => `const n${i}=new Codec<uint_32>(); const s${i}=new Codec<string>();`,
).join('\n');
const output = new ProtobufCompiler().compile(imports + codec + declarations, 'reuse.ts')!;
expect(output.match(/function protobuf_encode_Box__uint_32\(/g)).toHaveLength(1);
expect(output.match(/function protobuf_decode_Box__uint_32\(/g)).toHaveLength(1);
expect(output.match(/function protobuf_encode_Box__string\(/g)).toHaveLength(1);
expect(output.match(/function protobuf_decode_Box__string\(/g)).toHaveLength(1);
});
it('alternates method type arguments on one instance', () => {
expect(
run(`${box}
class Codec {roundtrip<T>(value:Box<T>) {return protobuf_decode<Box<T>>(protobuf_encode<Box<T>>(value));}}
const instance=new Codec();
export const result=[instance.roundtrip<uint_32>({value:7}),instance.roundtrip<string>({value:'x'}),instance.roundtrip<uint_32>({value:8})];
`).result,
).toEqual([{ value: 7 }, { value: 'x' }, { value: 8 }]);
});
it('forwards outer generics through factories returning class instances', () => {
expect(
run(`${codec}
function create<T>() {return new Codec<T>();}
const n=create<uint_32>(); const s=create<string>();
export const result=[n.decode(n.encode({value:7})),s.decode(s.encode({value:'x'}))];
`).result,
).toEqual([{ value: 7 }, { value: 'x' }]);
});
it('combines factory class arguments and independent method arguments', () => {
expect(
run(`interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
class Codec<T> {roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
function create<T>():Codec<T> {return new Codec<T>();}
const n=create<uint_32>(); const s=create<string>(); const alias=n;
export const result=[n.roundtrip<string>({first:7,second:'x'}),s.roundtrip<uint_32>({first:'y',second:8}),alias.roundtrip<uint_32>({first:9,second:10}),create<string>().roundtrip<string>({first:'a',second:'b'})];
`).result,
).toEqual([
{ first: 7, second: 'x' },
{ first: 'y', second: 8 },
{ first: 9, second: 10 },
{ first: 'a', second: 'b' },
]);
});
it('infers constructor generics from annotated arguments', () => {
expect(
run(`${box}
class Codec<T> {constructor(readonly value:Box<T>) {} encode(){return protobuf_encode<Box<T>>(this.value);}}
const n:Box<uint_32>={value:7}; const s:Box<string>={value:'x'};
const numbers=new Codec(n); const strings=new Codec(s);
export const result=[[...numbers.encode()],[...strings.encode()]];
`).result,
).toEqual([
[8, 7],
[10, 1, 120],
]);
});
it('infers factories and inherited constructors with renamed parameters', () => {
expect(
run(`${box}
class Codec<T> {constructor(readonly value:Box<T>) {} encode(){return protobuf_encode<Box<T>>(this.value);}}
class Middle<V> extends Codec<V> {}
class Derived<U> extends Middle<U> {}
function create<W>(value:Box<W>) {return new Derived(value);}
const n:Box<uint_32>={value:7}; const s:Box<string>={value:'x'};
const numbers=create(n); const strings=create(s);
export const result=[[...numbers.encode()],[...strings.encode()]];
`).result,
).toEqual([
[8, 7],
[10, 1, 120],
]);
});
it('specializes unannotated factories, aliases and default class arguments', () => {
expect(
run(`interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
class Codec<T=string> {roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
const create=<T>()=>new Codec<T>();
type Alias<T> = Codec<T>;
function use<T>(instance:Alias<T>, first:T) {return instance.roundtrip<string>({first,second:'ok'});}
export const result=[create<uint_32>().roundtrip<string>({first:7,second:'x'}),new Codec().roundtrip<uint_32>({first:'y',second:8}),use<string>(create<string>(),'z')];
`).result,
).toEqual([
{ first: 7, second: 'x' },
{ first: 'y', second: 8 },
{ first: 'z', second: 'ok' },
]);
});
it('retains class bindings in generic callback adapters', () => {
expect(
run(`interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
class Codec<T> {roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
function create<T>():Codec<T> {return new Codec<T>();}
const numbers=create<uint_32>(); const strings=create<string>();
const n=numbers.roundtrip<string>.bind(numbers); const s=strings.roundtrip<uint_32>.bind(strings);
export const result=[n({first:7,second:'x'}),s({first:'y',second:8})];
`).result,
).toEqual([
{ first: 7, second: 'x' },
{ first: 'y', second: 8 },
]);
});
it('separates specializations through inherited constructors', () => {
expect(
run(`${codec}
class Derived<T> extends Codec<T> {}
const n=new Derived<uint_32>(); const s=new Derived<string>();
export const result=[n.decode(n.encode({value:7})),s.decode(s.encode({value:'x'}))];
`).result,
).toEqual([{ value: 7 }, { value: 'x' }]);
});
it('keeps cross-file factory instances specialized', async () => {
expect(
await bundle({
'codec.ts': `${imports} ${codec} export function create<T>() {return new Codec<T>();}`,
'entry.ts': `import {create} from './codec'; const n=create<uint_32>();const s=create<string>(); export const result=[n.decode(n.encode({value:7})),s.decode(s.encode({value:'x'}))];`,
}),
).toEqual({ result: [{ value: 7 }, { value: 'x' }] });
});
it('resolves method defaults from the class type argument', () => {
expect(
run(`interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
class Codec<T> {roundtrip<U=T>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
function create<T>():Codec<T> {return new Codec<T>();}
export const result=[create<uint_32>().roundtrip({first:7,second:8}),create<string>().roundtrip({first:'x',second:'y'})];
`).result,
).toEqual([
{ first: 7, second: 8 },
{ first: 'x', second: 'y' },
]);
});
it('specializes inherited static generic methods', () => {
expect(
run(`${box}
class Base {static roundtrip<T>(value:Box<T>) {return protobuf_decode<Box<T>>(protobuf_encode<Box<T>>(value));}}
class Codec extends Base {}
export const result=[Codec.roundtrip<uint_32>({value:7}),Codec.roundtrip<string>({value:'x'})];
`).result,
).toEqual([{ value: 7 }, { value: 'x' }]);
});
it('combines cross-file class and method generics', async () => {
expect(
await bundle({
'codec.ts': `${imports} export interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
class Codec<T> {roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
export const create=<T>()=>new Codec<T>();`,
'entry.ts': `import {create} from './codec'; const n=create<uint_32>(); const s=create<string>();
export const result=[n.roundtrip<string>({first:7,second:'x'}),s.roundtrip<uint_32>({first:'y',second:8})];`,
}),
).toEqual({
result: [
{ first: 7, second: 'x' },
{ first: 'y', second: 8 },
],
});
});
});
@@ -0,0 +1,424 @@
import { bundle } from '../project';
import { packageName } from '../../../src/package';
import { describe, expect, it } from 'vitest';
import { ProtobufCompiler } from '../../../src/compiler/compiler';
import { run, imports } from '../compile';
describe('semantic compiler', () => {
it('invalidates preloaded schema types when the transform receives different source', () => {
const compiler = new ProtobufCompiler();
const source = `${imports} interface Msg {value:pb<1,uint_32>;} protobuf_encode<Msg>({value:7});`;
compiler.context.source('preloaded.ts', source);
compiler.prepare(['preloaded.ts']);
expect(compiler.compile(source.replace('pb<1,', 'pb<2,'), 'preloaded.ts')).toContain(
'buf[offset++] = 16;',
);
});
it('preserves async generic function bodies and return values', async () => {
const result = run(`interface Msg { value: pb<1,uint_32>; }
async function encode<T>(value:T) { await Promise.resolve(); return protobuf_encode<T>(value); }
export const result = encode<Msg>({value:7});
`);
expect([...(await result.result)]).toEqual([8, 7]);
});
it('supports namespace markers and namespace runtime functions', () => {
expect(
run(`
import * as p from '${packageName}';
interface Msg { value: p.pb<1, p.uint_32>; }
export const result = [...p.protobuf_encode<Msg>({value: 7})];
`).result,
).toEqual([8, 7]);
});
it('supports default generic classes and re-exported runtime bindings', async () => {
expect(
await bundle({
'runtime.ts': `export { protobuf_encode as encode } from '${packageName}';`,
'codec.ts': `import { encode } from './runtime'; export default class Codec<T> { encode(value:T) { return encode<T>(value); } }`,
'entry.ts': `import Codec from './codec'; interface Msg { value: pb<1,uint_32>; } const codec = new Codec<Msg>(); export const result = [...codec.encode({value:7})];`,
}),
).toEqual({ result: [8, 7] });
});
it('resolves generic parameters containing dollar signs without text substitution', () => {
expect(
run(`interface Box<$T> { value: pb<1,$T>; }
function encode<$T>(value:Box<$T>) { return protobuf_encode<Box<$T>>(value); }
export const result = [...encode<string>({value:'x'})];
`).result,
).toEqual([10, 1, 120]);
});
it('preserves directive prologues', () => {
const code = `'use client';\n${imports}\ninterface Msg { value: pb<1,uint_32>; } protobuf_encode<Msg>({value:7});`;
expect(new ProtobufCompiler().compile(code, 'directive.ts')).toMatch(/^'use client';/);
});
it('does not mix anonymous fields instantiated with different generic arguments', () => {
expect(
run(`
interface Box<T> { nested: pb<1, { value: pb<1, T>; }>; }
export const a = protobuf_decode<Box<uint_32>>(protobuf_encode<Box<uint_32>>({ nested: { value: 7 } }));
export const b = protobuf_decode<Box<string>>(protobuf_encode<Box<string>>({ nested: { value: 'x' } }));
`),
).toEqual({ a: { nested: { value: 7 } }, b: { nested: { value: 'x' } } });
});
it('evaluates conditional schema types structurally', () => {
expect(
run(`
interface Base { id: pb<1, uint_32>; }
interface Child extends Base { text: pb<2, string>; }
type Chosen<T> = T extends Base ? Child : never;
export const result = protobuf_decode<Chosen<Child>>(protobuf_encode<Chosen<Child>>({ id: 7, text: 'x' }));
`).result,
).toEqual({ id: 7, text: 'x' });
});
it('supports destructured generic object methods and aliases', () => {
expect(
run(`
interface Msg { value: pb<1, uint_32>; }
const api = { encode<T>(value: T) { return protobuf_encode<T>(value); } };
const { encode: send } = api;
export const result = [...send<Msg>({ value: 7 })];
`).result,
).toEqual([8, 7]);
});
it('rejects generic callbacks without a concrete adapter', () => {
expect(() =>
run(`
interface Msg { value: pb<1, uint_32>; }
function encode<T>(value: T) { return protobuf_encode<T>(value); }
[{ value: 7 }].map(encode);
`),
).toThrow(/Cannot infer callback schema/);
});
it('rejects detached instance codec methods', () => {
expect(() =>
run(`interface Msg { value: pb<1,uint_32>; }
class Codec<T> { encode(value:T) { return protobuf_encode<T>(value); } }
const codec = new Codec<Msg>(); const encode = codec.encode;
`),
).toThrow(/receiver/);
});
it('rejects class field calls that run before codec initialization', () => {
expect(() => run(`class Codec<T> { value = protobuf_encode<T>({} as T); }`)).toThrow(
/field initializers/,
);
});
it('keeps function length stable after generic codec injection', () => {
expect(
run(`
interface Msg { value: pb<1, uint_32>; }
function encode<T>(value: T) { return protobuf_encode<T>(value); }
export const length = encode.length;
export const result = [...encode<Msg>({ value: 7 })];
`),
).toEqual({ length: 1, result: [8, 7] });
});
it('reuses exact transform results and clears changed-source caches', () => {
const compiler = new ProtobufCompiler();
const source = `${imports} interface Msg { value: pb<1, uint_32>; } protobuf_encode<Msg>({value: 7});`;
const first = compiler.compile(source, 'cache.ts');
expect(compiler.compile(source, 'cache.ts')).toBe(first);
const second = compiler.compile(source.replace('pb<1,', 'pb<2,'), 'cache.ts');
expect(second).toContain('buf[offset++] = 16;');
expect(second).not.toEqual(first);
});
it('resolves tsconfig path aliases', async () => {
expect(
await bundle({
'tsconfig.json': JSON.stringify({
compilerOptions: { baseUrl: '.', paths: { '@schema/*': ['schema/*'] } },
}),
'schema/msg.ts': 'export interface Msg { value: pb<1, uint_32>; }',
'entry.ts': `${imports} import type { Msg } from '@schema/msg'; export const result = [...protobuf_encode<Msg>({value:7})];`,
}),
).toEqual({ result: [8, 7] });
});
it('supports recursive generic aliases and nested generic inference', () => {
expect(
run(`
type Node<T> = { value: pb<1, T>; next?: pb<2, Node<T>>; };
function encode<T>(value: Node<T>) { return protobuf_encode<Node<T>>(value); }
const value: Node<string> = { value: 'a', next: { value: 'b' } };
export const result = protobuf_decode<Node<string>>(encode(value));
`).result,
).toEqual({ value: 'a', next: { value: 'b', next: null } });
});
it('supports inferred generic forwarding and inherited generic methods', () => {
expect(
run(`
interface Pair<T, U> { a: pb<1, T>; b: pb<2, U>; }
class Base<T> { encode<U>(v: Pair<T, U>) { return protobuf_encode<Pair<T, U>>(v); } }
class Derived<T> extends Base<T> {}
const codec = new Derived<uint_32>();
export const result = [...codec.encode<string>({ a: 7, b: 'x' })];
`).result,
).toEqual([8, 7, 18, 1, 120]);
});
it('keeps generated helper names and user property strings separate', () => {
expect(
run(`
const __td = 'user decoder';
const protobuf_encode_Msg = 'user encoder';
interface Msg { '__td': pb<1, string>; }
export const result = protobuf_decode<Msg>(protobuf_encode<Msg>({ __td: 'ok' }));
export const untouched = [__td, protobuf_encode_Msg];
`),
).toEqual({ result: { __td: 'ok' }, untouched: ['user decoder', 'user encoder'] });
});
it('shares codecs across distinct instantiations without mixing schemas', () => {
expect(
run(`
interface A { value: pb<1, uint_32>; }
interface B { value: pb<2, string>; }
const encode = <T>(value: T) => protobuf_encode<T>(value);
export const result = [[...encode<A>({ value: 7 })], [...encode<B>({ value: 'x' })]];
`).result,
).toEqual([
[8, 7],
[18, 1, 120],
]);
});
it('supports generic field-number aliases and preserves mapped optionality', () => {
expect(
run(`
type Field<N extends number, T> = pb_optional<N, T>;
interface Msg { value?: Field<3, uint_32>; }
type Copy<T> = { [K in keyof T]: T[K] };
export const result = [...protobuf_encode<Copy<Msg>>({ value: 0 })];
`).result,
).toEqual([24, 0]);
});
it('forwards codecs through implicit and explicit generic derived constructors', () => {
expect(
run(`
interface Msg { value: pb<1, uint_32>; }
class Base<T> {
constructor(public label = 'base') {}
encode(value: T) { return protobuf_encode<T>(value); }
}
class Middle<T> extends Base<T> {}
class Derived<T> extends Middle<T> {
constructor(label: string) { super(label); }
}
const codec = new Derived<Msg>('ok');
export const result = [...codec.encode({ value: 7 })];
export const label = codec.label;
`),
).toEqual({ result: [8, 7], label: 'ok' });
});
it('forwards concrete codecs through a non-generic derived class', () => {
expect(
run(`
interface Msg { value: pb<1, uint_32>; }
class Base<T> { encode(value: T) { return protobuf_encode<T>(value); } }
class Codec extends Base<Msg> {}
export const result = [...new Codec().encode({ value: 7 })];
`).result,
).toEqual([8, 7]);
});
it('preserves explicitly present optional default values and quoted field names', () => {
const result = run(`
interface Msg { 'a-b'?: pb_optional<1, uint_32>; flag?: pb_optional<2, bool>; text?: pb_optional<3, string>; big?: pb_optional<4, uint_64>; '__proto__'?: pb<5, string>; }
const input = { 'a-b': 0, flag: false, text: '', big: 0n, ['__proto__']: 'safe' };
export const result = protobuf_decode<Msg>(protobuf_encode<Msg>(input));
`).result;
expect(result).toEqual({ 'a-b': 0, flag: false, text: '', big: 0n, ['__proto__']: 'safe' });
expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
});
it('preserves imported generic wrappers and transitive aliases across .js imports and barrels', async () => {
const result = await bundle({
'types.ts': `export interface Box<T> { value: pb<1, T>; }`,
'codec.ts': `${imports} import type { Box as B } from './types.js';
let count = 0;
export function wrap<T>(value: B<T>) { count++; return protobuf_encode<B<T>>(value); }
export function calls() { return count; }`,
'barrel.ts': `export { wrap as encode, calls } from './codec.js'; export type { Box as Shape } from './types.js';`,
'entry.ts': `${imports} import { encode, calls } from './barrel.js'; import type { Shape } from './barrel.js';
export const result = protobuf_decode<Shape<string>>(encode<string>({ value: 'ok' }));
export const count = calls();`,
});
expect(result).toEqual({ result: { value: 'ok' }, count: 1 });
});
it('keeps same-named schemas from different files distinct', async () => {
const result = await bundle({
'a.ts': `export interface Msg { value: pb<1, uint_32>; }`,
'b.ts': `export interface Msg { value: pb<2, string>; }`,
'entry.ts': `${imports} import type { Msg as A } from './a'; import type * as b from './b';
export const a = [...protobuf_encode<A>({ value: 7 })];
export const b = [...protobuf_encode<b.Msg>({ value: 'x' })];`,
});
expect(result).toEqual({ a: [8, 7], b: [18, 1, 120] });
});
it('supports generic arrow functions and rest parameters without reordering user arguments', () => {
expect(
run(`
interface Msg { value: pb<1, uint_32>; }
const encode = <T>(value: T, ...suffix: number[]) => [...protobuf_encode<T>(value), ...suffix];
export const result = encode<Msg>({ value: 7 }, 9, 10);
`).result,
).toEqual([8, 7, 9, 10]);
});
it('does not rewrite shadowed protobuf identifiers or unrelated object methods', () => {
expect(
run(`
interface Msg { value: pb<1, uint_32>; }
function custom(protobuf_encode: <T>(v: T) => number) { return protobuf_encode<Msg>({ value: 7 }); }
const unrelated = { encode<T>(value: T) { return value; } };
export const result = custom(() => 99) + unrelated.encode<number>(1);
`).result,
).toBe(100);
});
it('preserves generic wrapper branches, argument positions, closures and side effects', () => {
const result = run(`
interface Msg { value: pb<1, uint_32>; }
let count = 0;
function wrap<I, T>(ignored: I, data: T, enabled = true) {
count++;
if (!enabled) return new Uint8Array([99]);
return protobuf_encode<T>(data);
}
export const a = [...wrap<string, Msg>('unused', { value: 7 })];
export const b = [...wrap<string, Msg>('unused', { value: 8 }, false)];
export const calls = count;
`);
expect(result).toEqual({ a: [8, 7], b: [99], calls: 2 });
});
it('propagates concrete codecs through multiple generic functions and nested closures', () => {
const result = run(`
interface Box<T> { value: pb<1, T>; }
function inner<T>(value: Box<T>) { return protobuf_encode<Box<T>>(value); }
function outer<T>(value: Box<T>) {
function forward() { return inner<T>(value); }
return forward();
}
export const result = protobuf_decode<Box<string>>(outer<string>({ value: 'ok' }));
`);
expect(result.result).toEqual({ value: 'ok' });
});
it('resolves aliases, generic defaults, inheritance, intersections and indexed access', () => {
const result = run(`
type Scalar = uint_32;
interface Base<T = Scalar> { id: pb<1, T>; }
interface Extra { title: pb<2, string>; }
interface Child extends Base { title: Extra['title']; }
type Alias<T> = T;
type Shape = Alias<Pick<Child & Extra, 'id' | 'title'>>;
export const result = protobuf_decode<Shape>(protobuf_encode<Shape>({ id: 7, title: 'x' }));
`);
expect(result.result).toEqual({ id: 7, title: 'x' });
});
it('supports mapped and conditional generic types', () => {
const result = run(`
interface Msg { id: pb<1, uint_32>; text: pb<2, string>; }
type Copy<T> = { [K in keyof T]: T[K] };
type Choice<T> = T extends string ? Copy<Msg> : never;
export const result = protobuf_decode<Choice<string>>(protobuf_encode<Choice<string>>({ id: 5, text: 'ok' }));
`);
expect(result.result).toEqual({ id: 5, text: 'ok' });
});
it('passes class type parameters through constructor and instance methods', () => {
const result = run(`
interface Msg { value: pb<1, uint_32>; }
class Codec<T> {
constructor(public label = 'default') {}
encode(data: T) { return protobuf_encode<T>(data); }
decode(bytes: Uint8Array) { return protobuf_decode<T>(bytes); }
}
const a = new Codec<Msg>();
const b = new Codec<Msg>('named');
export const result = a.decode(b.encode({ value: 9 }));
export const labels = [a.label, b.label];
export const keys = Object.keys(a);
`);
expect(result).toEqual({
result: { value: 9 },
labels: ['default', 'named'],
keys: ['label'],
});
});
it('supports generic class methods with class and method type arguments', () => {
const result = run(`
interface Pair<T, U> { first: pb<1, T>; second: pb<2, U>; }
class Codec<T> {
encode<U>(value: Pair<T, U>) { return protobuf_encode<Pair<T, U>>(value); }
}
const codec = new Codec<uint_32>();
export const result = protobuf_decode<Pair<uint_32, string>>(codec.encode<string>({ first: 7, second: 'x' }));
`);
expect(result.result).toEqual({ first: 7, second: 'x' });
});
it('supports recursive message schemas and nullable links', () => {
const result = run(`
interface Node { value: pb<1, uint_32>; next?: pb<2, Node | undefined>; }
export const result = protobuf_decode<Node>(protobuf_encode<Node>({ value: 1, next: { value: 2 } }));
`);
expect(result.result).toEqual({ value: 1, next: { value: 2, next: null } });
});
it('infers a generic argument from an annotated variable', () => {
const result = run(`
interface Msg { value: pb<1, uint_32>; }
const data: Msg = { value: 7 };
function wrap<T>(value: T) { return protobuf_encode<T>(value); }
export const result = [...wrap(data)];
`);
expect(result.result).toEqual([8, 7]);
});
it('does not inspect unused unsupported schema declarations', () => {
expect(
run(`
interface Unused { value: pb<1, MissingType>; }
interface Msg { value: pb<1, uint_32>; }
export const result = [...protobuf_encode<Msg>({ value: 7 })];
`).result,
).toEqual([8, 7]);
});
it.each(['0', '-1', '1.5', '19000', '536870912'])(
'rejects invalid field number %s at compile time',
(number) => {
expect(() =>
run(
`interface Msg { value: pb<${number}, uint_32>; } protobuf_encode<Msg>({ value: 7 });`,
),
).toThrow(packageName);
},
);
it('rejects duplicate field numbers', () => {
expect(() =>
run('interface Msg { a: pb<1, uint_32>; b: pb<1, string>; } protobuf_encode<Msg>({});'),
).toThrow(/Duplicate/);
});
});
@@ -0,0 +1,201 @@
import { describe, expect, it } from 'vitest';
import { run, imports } from '../compile';
import { bundle } from '../project';
import { packageName } from '../../../src/package';
const schema =
'interface Box<T> {value:pb<1,T>;} interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}';
const codec = `${schema} class Codec<T> {
roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}
}`;
describe('generic composition', () => {
it('propagates two specializations across a 64-function call chain', () => {
const functions = Array.from(
{ length: 64 },
(_, i) =>
`function f${i}<T>(value:Box<T>) {return ${i === 63 ? 'protobuf_encode<Box<T>>(value)' : `f${i + 1}<T>(value)`};}`,
).join('\n');
expect(
run(
`${schema} ${functions} export const result=[[...f0<uint_32>({value:7})],[...f0<string>({value:'x'})]];`,
).result,
).toEqual([
[8, 7],
[10, 1, 120],
]);
});
it('composes deferred mapped types, promises, classes and polymorphic callbacks', () => {
expect(
run(`
interface Msg {id:pb<1,uint_32>;text:pb<2,string>;ignore:pb<3,bool>;}
type Payload<T> = Pick<Awaited<T>,'id'|'text'>;
interface Envelope<T> {value:pb<1,Payload<T>>;}
function encode<T>(value:Envelope<T>) {return protobuf_encode<Envelope<T>>(value);}
function apply<T>(value:Envelope<T>,fn:<U>(value:Envelope<U>)=>Uint8Array) {return fn<T>(value);}
class Codec<T> {encode(value:Envelope<T>){return apply<T>(value,encode);}}
const codec=new Codec<Promise<Msg>>();
export const result=protobuf_decode<Envelope<Msg>>(codec.encode({value:{id:7,text:'x'}}));
`).result,
).toEqual({ value: { id: 7, text: 'x' } });
});
it('retains outer type arguments across nested local factories', () => {
expect(
run(`${codec}
function outer<T>() {function inner() {return new Codec<T>();} return inner();}
const n=outer<uint_32>(); const s=outer<string>();
export const result=[n.roundtrip<string>({first:7,second:'x'}),s.roundtrip<uint_32>({first:'y',second:8})];
`).result,
).toEqual([
{ first: 7, second: 'x' },
{ first: 'y', second: 8 },
]);
});
it('resolves instances through parameter properties and getters', () => {
expect(
run(`${codec}
class Holder<T> {
constructor(readonly codec:Codec<T>) {}
get current():Codec<T> {return this.codec;}
}
const holder=new Holder<string>(new Codec<string>());
export const result=holder.current.roundtrip<uint_32>({first:'x',second:7});
`).result,
).toEqual({ first: 'x', second: 7 });
});
it('resolves static element access without changing this', () => {
expect(
run(`${codec}
const c=new Codec<uint_32>();
export const result=c['roundtrip']<string>({first:7,second:'x'});
`).result,
).toEqual({ first: 7, second: 'x' });
});
it('supports overloaded generic methods and constructors', () => {
expect(
run(`${schema}
class Codec<T> {
value:Box<T>;
constructor(value:Box<T>);
constructor(value:Box<T>, label:string);
constructor(value:Box<T>, public label='default') {this.value=value;}
encode() {return protobuf_encode<Box<T>>(this.value);}
roundtrip<U>(value:Box<U>):Box<U>;
roundtrip<U>(value:Box<U>):Box<U> {return protobuf_decode<Box<U>>(protobuf_encode<Box<U>>(value));}
}
const n:Box<uint_32>={value:7}; const c=new Codec(n);
export const result=[[...c.encode()],c.roundtrip<string>({value:'x'}),c.label];
`).result,
).toEqual([[8, 7], { value: 'x' }, 'default']);
});
it('forwards shadowed generic names through deeply nested callbacks', () => {
expect(
run(`${schema}
function roundtrip<T>(value:Box<T>) {return protobuf_decode<Box<T>>(protobuf_encode<Box<T>>(value));}
function outer<T>(first:Box<T>, fn:<T>(value:Box<T>)=>Box<T>) {
function middle<U>(next:Box<U>) {
function inner<T>(value:Box<T>) {return fn<T>(value);}
return [inner<T>(first),inner<U>(next)];
}
return middle<string>({value:'x'});
}
export const result=outer<uint_32>({value:7},roundtrip);
`).result,
).toEqual([{ value: 7 }, { value: 'x' }]);
});
it('converges through mutually recursive generic functions', () => {
expect(
run(`${schema}
function a<T>(value:Box<T>, depth:number):Box<T> {return depth ? b<T>(value,depth-1) : protobuf_decode<Box<T>>(protobuf_encode<Box<T>>(value));}
function b<U>(value:Box<U>, depth:number):Box<U> {return a<U>(value,depth);}
export const result=[a<uint_32>({value:7},32),a<string>({value:'x'},33)];
`).result,
).toEqual([{ value: 7 }, { value: 'x' }]);
});
it('keeps class factory specializations through cyclic re-export modules', async () => {
expect(
await bundle({
'codec.ts': `${imports} ${codec} export const create=<T>()=>new Codec<T>();`,
'first.ts': `export * from './second'; export {create} from './codec';`,
'second.ts': `export * from './first';`,
'entry.ts': `import {create} from './second'; export const result=create<string>().roundtrip<uint_32>({first:'x',second:7});`,
}),
).toEqual({ result: { first: 'x', second: 7 } });
});
it('recognizes parenthesized runtime aliases and namespace element access', () => {
expect(
run(`${schema} import * as runtime from '${packageName}';
const encode=protobuf_encode; const alias=encode;
export const result=[
[...(alias)<Box<uint_32>>({value:7})],
runtime['protobuf_decode']<Box<string>>(runtime['protobuf_encode']<Box<string>>({value:'x'}))
];
`).result,
).toEqual([[8, 7], { value: 'x' }]);
});
it('adapts typed callbacks selected by static element access', () => {
expect(
run(`${codec}
const c=new Codec<uint_32>();
const callback:(value:Pair<uint_32,string>)=>Pair<uint_32,string> = c['roundtrip'].bind(c);
export const result=callback({first:7,second:'x'});
`).result,
).toEqual({ first: 7, second: 'x' });
});
it('captures outer generics in a local class returned from a factory', () => {
expect(
run(`${schema}
function create<T>() {
class Local {roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
return new Local();
}
export const result=[create<uint_32>().roundtrip<string>({first:7,second:'x'}),create<string>().roundtrip<uint_32>({first:'y',second:8})];
`).result,
).toEqual([
{ first: 7, second: 'x' },
{ first: 'y', second: 8 },
]);
});
it('resolves namespaced base schemas and separates imported type and value names', async () => {
expect(
await bundle({
'types.ts': `export interface Base<T> {value:pb<1,T>;} export const Base=123;`,
'entry.ts': `${imports} import type * as types from './types';
interface Msg<T> extends types.Base<T> {text:pb<2,string>;}
export const result=protobuf_decode<Msg<uint_32>>(protobuf_encode<Msg<uint_32>>({value:7,text:'x'}));`,
}),
).toEqual({ result: { value: 7, text: 'x' } });
});
it('keeps valid recursive references through inherited generic fields', () => {
expect(
run(`
interface Base<T> {next:pb<1,Link<T>|null>;}
interface Link<T> extends Base<T> {value:pb<2,T>;}
const value:Link<string>={value:'a',next:{value:'b',next:null}};
export const result=protobuf_decode<Link<string>>(protobuf_encode<Link<string>>(value));
`).result,
).toEqual({ value: 'a', next: { value: 'b', next: null } });
});
it('resolves imported type and value names independently of declaration order', async () => {
expect(
await bundle({
'types.ts': `export const Msg=42; export interface Msg {value:pb<1,uint_32>;}`,
'barrel.ts': `export {Msg} from './types';`,
'entry.ts': `${imports} import {Msg} from './barrel'; export const result=[Msg,[...protobuf_encode<Msg>({value:7})]];`,
}),
).toEqual({ result: [42, [8, 7]] });
});
});
@@ -0,0 +1,114 @@
import { describe, expect, it } from 'vitest';
import { ProtobufCompiler } from '../../../src/compiler/compiler';
import { imports, run } from '../compile';
import { withProject } from '../project';
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';
describe('compiler failure recovery', () => {
it('rejects circular class inheritance without looping during member resolution', () => {
expect(() =>
run(
`class A<T> extends B<T> {} class B<U> extends A<U> {} new A<uint_32>().missing();`,
),
).toThrow(/Circular class inheritance/);
});
it('rejects circular schema inheritance without caching an empty message', () => {
expect(() =>
run(`interface A extends B {} interface B extends A {} protobuf_encode<A>({});`),
).toThrow(/Circular schema inheritance/);
});
it('does not cache a partially validated schema after failure', () => {
const compiler = new ProtobufCompiler();
const source = `${imports} interface Msg {a:pb<1,uint_32>;b:pb<1,string>;} protobuf_encode<Msg>({a:7,b:'x'});`;
for (let i = 0; i < 3; i++) {
expect(() => compiler.compile(source, 'invalid.ts')).toThrow(
/Duplicate protobuf field number/,
);
}
expect(compiler.compile(source.replace('b:pb<1,', 'b:pb<2,'), 'invalid.ts')).toContain(
'protobuf_encode_Msg',
);
});
it('reports expanding recursive schema aliases before overflowing the stack', () => {
expect(() =>
run(
`type Grow<T>={value:pb<1,T>;next?:pb<2,Grow<Grow<T>>>;}; protobuf_encode<Grow<uint_32>>({value:7});`,
),
).toThrow(/128 levels|does not converge/);
});
it('reports circular aliases with a source location', () => {
expect(() => run(`type A=B; type B=A; protobuf_encode<A>({});`)).toThrow(
/test-entry.ts:\d+:\d+: Circular type alias/,
);
});
it('replaces specialized code when the same module repeatedly changes wire types', () => {
const compiler = new ProtobufCompiler();
for (let i = 0; i < 32; i++) {
const type = i % 2 ? 'string' : 'uint_32';
const other = i % 2 ? 'uint_32' : 'string';
const source = `${imports} interface Box<T> {value:pb<1,T>;} class Codec<T> {encode(value:Box<T>) {return protobuf_encode<Box<T>>(value);}} new Codec<${type}>();`;
const output = compiler.compile(source, 'changing.ts');
expect(output).toContain(`function protobuf_encode_Box__${type}(`);
expect(output).not.toContain(`function protobuf_encode_Box__${other}(`);
}
});
it('rejects ambiguous factory specializations with a build diagnostic', () => {
expect(() =>
run(`interface Box<T> {value:pb<1,T>;}
class Codec<T> {encode<U>(value:Box<T>) {return protobuf_encode<Box<T>>(value);}}
function create(flag:boolean) {if(flag) return new Codec<uint_32>();return new Codec<string>();}
create(true).encode<bool>({value:7});
`),
).toThrow(/Receiver branches must resolve to the same specialization/);
});
it('reconstructs all entry requirements after a failed compilation', async () => {
const shared =
'export function apply<T>(value:T, fn:<U>(value:U)=>Uint8Array) {return fn<T>(value);}';
await withProject(
{
'shared.ts': shared,
'a.ts': `${imports} import {apply} from './shared'; interface Msg {value:pb<1,uint_32>;} function encode<T>(value:T) {return protobuf_encode<T>(value);} apply<Msg>({value:7},encode);`,
'b.ts': `${imports} import {apply} from './shared'; interface Msg {value:pb<1,uint_32>;} interface Box<T> {value:pb<1,T>;} function encode<T>(value:T) {return protobuf_encode<Box<T>>({value});} apply<Msg>({value:8},encode);`,
},
(directory) => {
const compiler = new ProtobufCompiler();
compiler.prepare([join(directory, 'a.ts'), join(directory, 'b.ts')]);
const baseline = compiler.compile(shared, join(directory, 'shared.ts'));
expect(() =>
compiler.compile(
`${imports} protobuf_encode<Missing>({});`,
join(directory, 'bad.ts'),
),
).toThrow(/Unresolved type/);
expect(compiler.compile(shared, join(directory, 'shared.ts'))).toBe(baseline);
},
);
});
it('recovers when entry preparation fails before a corrected source is saved', async () => {
const source = `${imports} interface Msg {value:pb<1,uint_32>;} function encode<T>(value:T) {return protobuf_encode<T>(value);} encode({value:7});`;
await withProject({ 'entry.ts': source }, (directory) => {
const compiler = new ProtobufCompiler();
const file = join(directory, 'entry.ts');
for (let i = 0; i < 2; i++) {
expect(() => compiler.prepare([file])).toThrow(/Cannot infer T/);
}
const corrected = source.replace('encode({', 'encode<Msg>({');
writeFileSync(file, corrected);
compiler.prepare([file]);
expect(compiler.compile(corrected, file)).toContain('protobuf_encode_Msg');
});
});
});
@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest';
import { run } from '../compile';
const schema = 'interface Msg {value:pb<1,uint_32>;}';
describe('call evaluation', () => {
it('preserves optional receiver short circuiting and single evaluation', () => {
expect(
run(`${schema}
const events:string[]=[];
class Codec {encode<T>(value:T) {events.push('encode');return protobuf_encode<T>(value);}}
function get(present:boolean):Codec|undefined {events.push('get');return present ? new Codec() : undefined;}
function argument():Msg {events.push('argument');return {value:7};}
export const absent=get(false)?.encode<Msg>(argument());
export const result=[...get(true)?.encode<Msg>(argument())!];
export {events};
`),
).toEqual({
absent: undefined,
result: [8, 7],
events: ['get', 'get', 'argument', 'encode'],
});
});
it('preserves getter receivers, multiple spreads and trailing commas', () => {
expect(
run(`${schema}
const events:string[]=[];
const codec={encode<T>(value:T,...labels:string[]) {events.push(...labels);return protobuf_encode<T>(value);}};
const holder={get current(){events.push('get');return codec;}};
function values():[Msg] {events.push('values');return [{value:7}];}
function labels():string[] {events.push('labels');return ['a','b'];}
export const result=[...holder.current.encode<Msg>(...values(),...labels(),)];
export {events};
`),
).toEqual({ result: [8, 7], events: ['get', 'values', 'labels', 'a', 'b'] });
});
it('evaluates constructor defaults before the body and preserves rest arguments', () => {
expect(
run(`${schema}
const events:string[]=[];
class Codec<T> {
constructor(public value:T, public bytes=protobuf_encode<T>(value), ...labels:string[]) {events.push(...labels);}
decode() {return protobuf_decode<T>(this.bytes);}
}
function args():[Msg,undefined,string,string] {events.push('args');return [{value:7},undefined,'a','b'];}
const c=new Codec<Msg>(...args());
export const result=c.decode(); export {events};
export const length=Codec.length;
`),
).toEqual({ result: { value: 7 }, events: ['args', 'a', 'b'], length: 1 });
});
it('preserves default codec calls in generic callback implementations', () => {
expect(
run(`${schema}
function encode<T>(value:T, bytes=protobuf_encode<T>(value)) {return bytes;}
function apply<T>(value:T, fn:<U>(value:U, bytes?:Uint8Array)=>Uint8Array) {return fn<T>(value);}
export const result=[...apply<Msg>({value:7},encode)];
`).result,
).toEqual([8, 7]);
});
it('supports generator and async generator bodies', async () => {
const result = run(`${schema}
function* encode<T>(value:T) {yield protobuf_encode<T>(value);}
async function* later<T>(value:T) {yield protobuf_encode<T>(value);}
export const result=[...encode<Msg>({value:7})].map(bytes=>[...bytes]);
export const pending=later<Msg>({value:8}).next();
`);
expect(result.result).toEqual([[8, 7]]);
expect([...(await result.pending).value]).toEqual([8, 8]);
});
});
@@ -0,0 +1,54 @@
import { bundle } from '../project';
import { describe, expect, it } from 'vitest';
import { imports } from '../compile';
describe('module graph', () => {
it('resolves nested imported schemas and renamed exports', async () => {
expect(
await bundle({
'inner.ts': 'export interface Inner {value:pb<1,uint_32>;} ',
'outer.ts':
"import type {Inner} from './inner'; export interface Outer {inner:pb<1,Inner>;}",
'barrel.ts': "export {Outer as Message} from './outer';",
'entry.ts': `${imports} import type {Message as Msg} from './barrel'; export const result = protobuf_decode<Msg>(protobuf_encode<Msg>({inner:{value:7}}));`,
}),
).toEqual({ result: { inner: { value: 7 } } });
});
it('keeps wrapper control flow, object receivers and transitive generic aliases', async () => {
expect(
await bundle({
'schema.ts':
'export interface Box<T> {value:pb<1,T>;} export type Message<T> = Box<Box<T>>;',
'codec.ts': `${imports} import type {Message} from './schema';
export const api = { count:0, encode<T>(value:Message<T>, enabled=true) {
this.count++; if (!enabled) return new Uint8Array();
const inner = () => protobuf_encode<Message<T>>(value); return inner();
}};`,
'entry.ts': `${imports} import {api} from './codec'; import type {Message} from './schema';
export const result = protobuf_decode<Message<string>>(api.encode<string>({value:{value:'x'}}));
export const empty = api.encode<string>({value:{value:'x'}}, false).length;
export const count = api.count;`,
}),
).toEqual({ result: { value: { value: 'x' } }, empty: 0, count: 2 });
});
it('handles recursive types across modules', async () => {
expect(
await bundle({
'a.ts': "import type {B} from './b'; export interface A {value:pb<1,uint_32>; next?:pb<2,B>;}",
'b.ts': "import type {A} from './a'; export interface B {text:pb<1,string>; next?:pb<2,A>;}",
'entry.ts': `${imports} import type {A} from './a'; export const result = protobuf_decode<A>(protobuf_encode<A>({value:7,next:{text:'x'}}));`,
}),
).toEqual({ result: { value: 7, next: { text: 'x', next: null } } });
});
it('resolves nested generic functions with non-first schema parameters', async () => {
expect(
await bundle({
'codec.ts': `${imports} export function send<Context,T>(context:Context, value:T) { if (!context) throw new Error('context'); return protobuf_encode<T>(value); }`,
'entry.ts': `import {send} from './codec'; interface Msg {value:pb<1,uint_32>;} export const result = [...send<string,Msg>('ok',{value:7})];`,
}),
).toEqual({ result: [8, 7] });
});
});
@@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest';
import { run } from '../compile';
const schema = 'interface Msg {id:pb<1,uint_32>; text:pb<2,string>;}';
describe('schema type transformations', () => {
it('uses class parameter properties and ignores static metadata', () => {
expect(
run(`
class Msg<T> {static label='message';constructor(public value:pb<1,T>) {}}
export const result=protobuf_decode<Msg<uint_32>>(protobuf_encode<Msg<uint_32>>(new Msg<uint_32>(7)));
`).result,
).toEqual({ value: 7 });
});
it('captures function generics in local schemas without mixing instantiations', () => {
expect(
run(`
function encode<T>(value:T) { interface Local {value:pb<1,T>;} return protobuf_encode<Local>({value}); }
export const result=[[...encode<uint_32>(7)], [...encode<string>('x')]];
`).result,
).toEqual([
[8, 7],
[10, 1, 120],
]);
});
it('extracts schema types through infer, functions and promises', () => {
expect(
run(`${schema}
type Result<T> = T extends (...args:any[])=>infer R ? R : never;
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Schema = Unwrap<Result<(id:number)=>Promise<Promise<Msg>>>>;
export const result = protobuf_decode<Schema>(protobuf_encode<Schema>({id:7,text:'x'}));
`).result,
).toEqual({ id: 7, text: 'x' });
});
it('supports Awaited, ReturnType, Parameters and type queries', () => {
expect(
run(`${schema}
function load(value:Msg): Promise<Msg> {return Promise.resolve(value);}
type Result = Awaited<ReturnType<typeof load>>;
type Input = Parameters<typeof load>[0];
export const result = protobuf_decode<Result>(protobuf_encode<Input>({id:7,text:'x'}));
`).result,
).toEqual({ id: 7, text: 'x' });
});
it('propagates deferred conditional transformations through generic functions', () => {
expect(
run(`${schema}
type Unwrap<T> = T extends Promise<infer U> ? U : T;
function encode<T>(value:Unwrap<T>) {return protobuf_encode<Unwrap<T>>(value);}
export const result = protobuf_decode<Msg>(encode<Promise<Msg>>({id:7,text:'x'}));
`).result,
).toEqual({ id: 7, text: 'x' });
});
it('remaps literal field names and omits never keys', () => {
expect(
run(`${schema}
type Rename<T> = {[K in keyof T as K extends 'text' ? never : \`field_\${Capitalize<K>}\`]:T[K]};
export const result = protobuf_decode<Rename<Msg>>(protobuf_encode<Rename<Msg>>({field_Id:7}));
`).result,
).toEqual({ field_Id: 7 });
});
it('selects explicit schemas with Extract and Exclude', () => {
expect(
run(`${schema}
type Schema = Extract<Msg|string,object>;
type Chosen = Exclude<Msg|string,string>;
export const result = protobuf_decode<Chosen>(protobuf_encode<Schema>({id:7,text:'x'}));
`).result,
).toEqual({ id: 7, text: 'x' });
});
it('supports constructor signatures and indexed tuple transformations', () => {
expect(
run(`${schema}
type Constructor = new(value:Msg)=>Msg;
type Instance = InstanceType<Constructor>;
type Input = ConstructorParameters<Constructor>[0];
export const result = protobuf_decode<Instance>(protobuf_encode<Input>({id:7,text:'x'}));
`).result,
).toEqual({ id: 7, text: 'x' });
});
});
@@ -0,0 +1,25 @@
import { expect, it, vi } from 'vitest';
vi.mock('../../../package.json', () => ({ name: '@test/renamed-codec' }));
import protobufPlugin from '../../../src/index';
import { ProtobufCompiler } from '../../../src/compiler/compiler';
import { protobuf_encode } from '../../../src/runtime';
it('uses the manifest name for imports, plugin identity and diagnostics', () => {
expect(protobufPlugin().name).toBe('@test/renamed-codec:vite');
expect(
new ProtobufCompiler().compile(
`import {protobuf_encode} from '@test/renamed-codec';
interface Msg {value:pb<1,uint_32>;} protobuf_encode<Msg>({value:7});`,
'renamed.ts',
),
).toContain('protobuf_encode_Msg({value:7})');
expect(() => protobuf_encode({})).toThrow('@test/renamed-codec Vite plugin');
expect(() =>
new ProtobufCompiler().compile(
`import {protobuf_encode} from '@test/renamed-codec'; protobuf_encode<Missing>({});`,
'error.ts',
),
).toThrow('[@test/renamed-codec]');
});
@@ -0,0 +1,108 @@
import { describe, expect, it } from 'vitest';
import protobufPlugin from '../../../src/index';
import { packageName } from '../../../src/package';
import { ProtobufCompiler } from '../../../src/compiler/compiler';
import { run, imports } from '../compile';
import { withProject } from '../project';
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';
const schema = 'interface Msg { value: pb<1,uint_32>; }';
describe('plugin entry', () => {
it('watches type-only dependencies and rebuilds importers after schema changes', async () => {
await withProject(
{ 'message.ts': 'export interface Msg { value: pb<1, uint_32>; }' },
(directory) => {
const plugin = protobufPlugin();
const transformHook = plugin.transform!;
const transform =
typeof transformHook === 'function' ? transformHook : transformHook.handler;
const dependencies: string[] = [];
const context = { addWatchFile: (path: string) => dependencies.push(path) } as any;
const file = join(directory, 'entry.ts').replaceAll('\\', '/');
const message = join(directory, 'message.ts').replaceAll('\\', '/');
const source = `${imports} import type { Msg } from './message'; protobuf_encode<Msg>({value:7});`;
const initial = transform.call(context, source, file) as { code: string };
expect(initial.code).toContain('buf[offset++] = 8;');
expect(dependencies).toContain(message);
writeFileSync(message, 'export interface Msg { value: pb<2, uint_32>; }');
const updateHook = plugin.handleHotUpdate!;
const update = typeof updateHook === 'function' ? updateHook : updateHook.handler;
const module = { id: file };
expect(
update.call(
{} as any,
{
file: message,
server: {
moduleGraph: {
getModuleById: (id: string) =>
id === file ? module : undefined,
},
},
} as any,
),
).toEqual([module]);
const updated = transform.call(context, source, file) as { code: string };
expect(updated.code).toContain('buf[offset++] = 16;');
},
);
});
it('recognizes the package name from package.json', () => {
const plugin = protobufPlugin();
const hook = plugin.transform!;
const transform = typeof hook === 'function' ? hook : hook.handler;
const result = transform.call(
{} as any,
`import {protobuf_encode as encode} from '${packageName}'; ${schema} encode<Msg>({value:7});`,
'entry.ts',
) as { code: string };
expect(result.code).toContain('protobuf_encode_Msg({value:7})');
});
it('leaves unrelated modules and declaration files alone', () => {
const plugin = protobufPlugin();
const hook = plugin.transform!;
const transform = typeof hook === 'function' ? hook : hook.handler;
for (const file of ['entry.js', 'schema.d.ts', 'schema.d.mts']) {
expect(transform.call({} as any, schema, file)).toBeNull();
}
expect(new ProtobufCompiler().compile(schema, 'schema.ts')).toBeNull();
});
it('generates only codecs reachable from actual calls', () => {
const code = new ProtobufCompiler().compile(
`import {protobuf_encode} from '${packageName}'; ${schema}
interface Unused {value:pb<1,Missing>;}
protobuf_encode<Msg>({value:7});`,
'used.ts',
)!;
expect(code).not.toContain('function protobuf_decode_');
expect(code).not.toContain('function protobuf_encode_Unused');
expect(code).toContain('function protobuf_encode_Msg');
});
it('reports unresolved types instead of leaving runtime calls behind', () => {
expect(() => run('protobuf_encode<Missing>({});')).toThrow(/Unresolved type Missing/);
});
it('fails clearly when the build plugin is absent', async () => {
const runtime = await import('../../../src/runtime');
expect(Object.keys(runtime).sort()).toEqual([
'protobuf_decode',
'protobuf_encode',
'protobuf_getUnknownFieldMetadata',
'protobuf_getUnknownFields',
]);
expect(() => runtime.protobuf_encode({})).toThrow(
`not transformed by the ${packageName} Vite plugin`,
);
expect(() => runtime.protobuf_decode(new Uint8Array())).toThrow(
`not transformed by the ${packageName} Vite plugin`,
);
});
});
+79
View File
@@ -0,0 +1,79 @@
import { build } from 'vite';
import protobufPlugin from '../../src/index';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname, posix } from 'node:path';
import { packageName } from '../../src/package';
export async function withProject<T>(
files: Record<string, string>,
action: (directory: string) => T | Promise<T>,
): Promise<T> {
const directory = mkdtempSync(join(tmpdir(), 'protobuf-compiler-'));
try {
for (const [name, text] of Object.entries(files)) {
const path = join(directory, name);
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, text);
}
return await action(directory);
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
export function bundle(
files: Record<string, string>,
entry: string | string[] = 'entry.ts',
): Promise<any> {
return withProject(files, async (directory) => {
const output = await build({
configFile: false,
root: directory,
logLevel: 'silent',
plugins: [protobufPlugin()],
build: {
write: false,
minify: false,
target: 'es2022',
lib: {
entry: Array.isArray(entry)
? entry.map((file) => join(directory, file))
: join(directory, entry),
formats: ['cjs'],
},
rolldownOptions: { external: [packageName] },
},
});
const result = (Array.isArray(output) ? output[0] : output) as {
output: { code?: string; fileName: string; name: string; isEntry?: boolean }[];
};
const cache = new Map<string, Record<string, unknown>>();
const load = (name: string): Record<string, unknown> => {
if (cache.has(name)) {
return cache.get(name)!;
}
const chunk = result.output.find((chunk) => chunk.fileName === name);
if (!chunk?.code) {
throw new Error(`Unexpected runtime dependency: ${name}`);
}
const exports = {};
cache.set(name, exports);
new Function('exports', 'require', chunk.code)(exports, (specifier: string) =>
load(posix.normalize(posix.join(posix.dirname(name), specifier))),
);
return Object.fromEntries(Object.entries(exports));
};
const entries = result.output.filter((chunk) => chunk.isEntry);
return Array.isArray(entry)
? Object.fromEntries(entries.map((chunk) => [chunk.name, load(chunk.fileName)]))
: load(entries[0].fileName);
});
}
+40
View File
@@ -0,0 +1,40 @@
import ts from 'typescript';
import { SourceContext } from '../../src/source/context';
import { SchemaResolver } from '../../src/schema/resolver';
import { generateCode } from '../../src/codegen/generator';
export function schemaCodecs<T>(code: string, name = 'Msg') {
const generated = generateCode(schemaRegistry(code));
return new Function(
`${generated}\nreturn { encode: protobuf_encode_${name}, decode: protobuf_decode_${name} };`,
)() as {
encode(value: T): Uint8Array;
decode(bytes: Uint8Array): T;
};
}
export function schemaRegistry(code: string, file = 'schema.ts') {
const context = new SourceContext();
const source = context.source(file, code);
const resolver = new SchemaResolver(context);
const roots: string[] = [];
for (const statement of source.statements) {
if (ts.isInterfaceDeclaration(statement) && !statement.typeParameters?.length) {
roots.push(statement.name.text);
}
}
const expanded = context.source(
file,
code + roots.map((name, i) => `\ntype __TestRoot${i} = ${name};`).join(''),
);
for (const statement of expanded.statements) {
if (ts.isTypeAliasDeclaration(statement) && statement.name.text.startsWith('__TestRoot')) {
resolver.message(resolver.type(statement.type), statement);
}
}
return resolver.registry;
}