Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,12 @@ For an example of a real-world usage of the library, see [vector-tile-js](https:
If installed globally, `pbf` provides a binary that compiles `proto` files into JavaScript modules. Usage:

```bash
$ pbf <proto_path> [--no-write] [--no-read] [--legacy]
$ pbf <proto_path> [--no-write] [--no-read] [--legacy] [--ts-declarations]
```

The `--no-write` and `--no-read` switches remove corresponding code in the output.
The `--legacy` switch makes it generate a CommonJS module instead of ESM.
The `--ts-declarations` switch makes it generate TypeScript declarations (corresponding to the ESM output).

`Pbf` will generate `read<Identifier>` and `write<Identifier>` functions for every message in the schema. For nested messages, their names will be concatenated — e.g. `Message` inside `Test` will produce `readTestMessage` and `writeTestMessage` functions.

Expand Down
15 changes: 11 additions & 4 deletions bin/pbf
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import resolve from 'resolve-protobuf-schema';
import {compileRaw} from '../compile.js';

if (process.argv.length < 3) {
console.error('Usage: pbf [file.proto] [--no-read] [--no-write] [--legacy]');
console.error('Usage: pbf [file.proto] [--no-read] [--no-write] [--legacy] [--ts-declarations]');
process.exit(0);
}

const code = compileRaw(resolve.sync(process.argv[2]), {
const options = {
noRead: process.argv.indexOf('--no-read') >= 0,
noWrite: process.argv.indexOf('--no-write') >= 0,
legacy: process.argv.indexOf('--legacy') >= 0
});
legacy: process.argv.indexOf('--legacy') >= 0,
tsDeclarations: process.argv.indexOf('--ts-declarations') >= 0
};

if (options.legacy && options.tsDeclarations) {
console.error('--legacy cannot be used with --ts-declarations');
process.exit(1);
}

const code = compileRaw(resolve.sync(process.argv[2]), options);
process.stdout.write(code);
140 changes: 140 additions & 0 deletions compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export function compile(proto) {

export function compileRaw(proto, options = {}) {
const context = buildDefaults(buildContext(proto, null), proto.syntax);
if (options.tsDeclarations) {
return `${options.dev ? '' : `// TypeScript declarations generated by pbf v${version}\n`}${writeTSContext(context, options)}`;
}
return `${options.dev ? '' : `// code generated by pbf v${version}\n`}${writeContext(context, options)}`;
}

Expand Down Expand Up @@ -415,3 +418,140 @@ function getDefaultWriteTest(ctx, field) {
function isPacked(field) {
return field.options.packed === 'true';
}

function writeTSContext(ctx, options) {
let code = '';
if (ctx._proto.fields) code += writeTSMessage(ctx, options);
if (ctx._proto.values) code += writeTSEnum(ctx, options);

for (let i = 0; i < ctx._children.length; i++) {
code += writeTSContext(ctx._children[i], options);
}
return code;
}

function writeTSMessage(ctx, options) {
const fields = ctx._proto.fields;
const name = ctx._name;

let code = '\n';

// Generate interface for the message
code += `export interface ${name} {\n`;

// Track oneof groups to handle them properly
const oneofGroups = new Map();
for (const field of fields) {
if (field.oneof) {
if (!oneofGroups.has(field.oneof)) {
oneofGroups.set(field.oneof, []);
}
oneofGroups.get(field.oneof).push(field);
}
}

for (const field of fields) {
const tsType = getTSType(ctx, field);

if (field.oneof) {
// For oneof fields, make them optional and add the oneof indicator
code += ` ${field.name}?: ${tsType} | null;\n`;
} else if (field.repeated) {
// Repeated fields are required arrays
code += ` ${field.name}: ${tsType};\n`;
} else {
// Regular optional fields
code += ` ${field.name}?: ${tsType} | null;\n`;
}
}

// Add oneof indicator properties
for (const [oneofName, oneofFields] of oneofGroups) {
const fieldNames = oneofFields.map(f => `"${f.name}"`).join(' | ');
code += ` ${oneofName}?: ${fieldNames};\n`;
}

code += '}\n\n';

// Generate read function declaration
if (!options.noRead) {
const readName = `read${name}`;
code += `export function ${readName}(pbf: any, end?: number): ${name};\n`;
}

// Generate write function declaration
if (!options.noWrite) {
const writeName = `write${name}`;
code += `export function ${writeName}(obj: ${name}, pbf: any): void;\n`;
}

return code;
}

function writeTSEnum(ctx) {
const name = ctx._name;
const values = getEnumValues(ctx);

let code = `export const ${name}: {\n`;
for (const [key, value] of Object.entries(values)) {
code += ` readonly ${key}: ${value};\n`;
}
code += '};\n\n';

return code;
}

function getTSType(ctx, field) {
if (field.type === 'map') {
const keyType = getTSTypeForFieldType(field.map.from, null, null);
const valueType = getTSTypeForFieldType(field.map.to, null, null);
return `{ [key: ${keyType}]: ${valueType} }`;
}

if (field.repeated) {
const elementType = getTSTypeForFieldType(field.type, ctx, field);
return `${elementType}[]`;
}

return getTSTypeForFieldType(field.type, ctx, field);
}

function getTSTypeForFieldType(type, ctx, field) {
if (ctx) {
const resolvedType = getType(ctx, {type});

if (resolvedType) {
if (resolvedType._proto.fields) {
return resolvedType._name;
}
if (resolvedType._proto.values) {
// For enums, return the type of the enum values (number)
return 'number';
}
}
}

// Check if field should use string as number (jstype=JS_STRING)
if (field && fieldShouldUseStringAsNumber(field)) {
return 'string';
}

switch (type) {
case 'string': return 'string';
case 'float':
case 'double': return 'number';
case 'bool': return 'boolean';
case 'uint32':
case 'uint64':
case 'int32':
case 'int64':
case 'sint32':
case 'sint64':
case 'fixed32':
case 'fixed64':
case 'sfixed32':
case 'sfixed64': return 'number';
case 'bytes': return 'Uint8Array';
default: return 'any';
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"scripts": {
"bench": "node bench/bench.js",
"pretest": "eslint *.js compile.js test/*.js test/fixtures/*.js bin/pbf",
"test": "tsc && node --test",
"test": "tsc && node --test test/*.js",
"cov": "node --test --experimental-test-covetage",
"build": "rollup -c",
"prepublishOnly": "npm run test && npm run build"
Expand Down
29 changes: 17 additions & 12 deletions test/compile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@ import {sync as resolve} from 'resolve-protobuf-schema';
import Pbf from '../index.js';
import {compile, compileRaw} from '../compile.js';

test('compiles all proto files to proper js', () => {
const files = fs.readdirSync(new URL('fixtures', import.meta.url));
function testProtoCompilation(options, extension, description) {
test(description, () => {
const files = fs.readdirSync(new URL('fixtures', import.meta.url));

for (const path of files) {
if (!path.endsWith('.proto')) continue;
const proto = resolve(new URL(`fixtures/${path}`, import.meta.url));
const js = compileRaw(proto, {dev: true});
for (const path of files) {
if (!path.endsWith('.proto')) continue;
const proto = resolve(new URL(`fixtures/${path}`, import.meta.url));
const code = compileRaw(proto, options);

// uncomment to update the fixtures
// fs.writeFileSync(new URL(`fixtures/${path}`.replace('.proto', '.js'), import.meta.url), js);
// uncomment to update the fixtures
// fs.writeFileSync(new URL(`fixtures/${path}`.replace('.proto', extension), import.meta.url), code);

const expectedJS = fs.readFileSync(new URL(`fixtures/${path}`.replace('.proto', '.js'), import.meta.url), 'utf8');
assert.equal(js, expectedJS);
}
});
const expectedCode = fs.readFileSync(new URL(`fixtures/${path}`.replace('.proto', extension), import.meta.url), 'utf8');
assert.equal(code, expectedCode);
}
});
}

testProtoCompilation({dev: true}, '.js', 'compiles all proto files to proper js');
testProtoCompilation({dev: true, tsDeclarations: true}, '.d.ts', 'compiles all proto files to TypeScript declarations');

test('compiles vector tile proto', () => {
const proto = resolve(new URL('fixtures/vector_tile.proto', import.meta.url));
Expand Down
16 changes: 16 additions & 0 deletions test/fixtures/defaults.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const MessageType: {
readonly UNKNOWN: 0;
readonly GREETING: 1;
};


export interface Envelope {
type?: number | null;
name?: string | null;
flag?: boolean | null;
weight?: number | null;
id?: number | null;
}

export function readEnvelope(pbf: any, end?: number): Envelope;
export function writeEnvelope(obj: Envelope, pbf: any): void;
27 changes: 27 additions & 0 deletions test/fixtures/defaults_implicit.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const MessageType: {
readonly UNKNOWN: 0;
readonly GREETING: 1;
};


export interface CustomType {
}

export function readCustomType(pbf: any, end?: number): CustomType;
export function writeCustomType(obj: CustomType, pbf: any): void;

export interface Envelope {
type?: number | null;
name?: string | null;
flag?: boolean | null;
weight?: number | null;
id?: number | null;
tags: string[];
numbers: number[];
bytes?: Uint8Array | null;
custom?: CustomType | null;
types: number[];
}

export function readEnvelope(pbf: any, end?: number): Envelope;
export function writeEnvelope(obj: Envelope, pbf: any): void;
16 changes: 16 additions & 0 deletions test/fixtures/defaults_proto3.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const MessageType: {
readonly UNKNOWN: 0;
readonly GREETING: 1;
};


export interface Envelope {
type?: number | null;
name?: string | null;
flag?: boolean | null;
weight?: number | null;
id?: number | null;
}

export function readEnvelope(pbf: any, end?: number): Envelope;
export function writeEnvelope(obj: Envelope, pbf: any): void;
23 changes: 23 additions & 0 deletions test/fixtures/embedded_type.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

export interface EmbeddedType {
value?: string | null;
sub_field?: EmbeddedTypeContainer | null;
sub_sub_field?: EmbeddedTypeContainerInner | null;
}

export function readEmbeddedType(pbf: any, end?: number): EmbeddedType;
export function writeEmbeddedType(obj: EmbeddedType, pbf: any): void;

export interface EmbeddedTypeContainer {
values: EmbeddedTypeContainerInner[];
}

export function readEmbeddedTypeContainer(pbf: any, end?: number): EmbeddedTypeContainer;
export function writeEmbeddedTypeContainer(obj: EmbeddedTypeContainer, pbf: any): void;

export interface EmbeddedTypeContainerInner {
value?: string | null;
}

export function readEmbeddedTypeContainerInner(pbf: any, end?: number): EmbeddedTypeContainerInner;
export function writeEmbeddedTypeContainerInner(obj: EmbeddedTypeContainerInner, pbf: any): void;
24 changes: 24 additions & 0 deletions test/fixtures/map.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

export interface Envelope {
kv?: { [key: string]: string } | null;
kn?: { [key: string]: number } | null;
}

export function readEnvelope(pbf: any, end?: number): Envelope;
export function writeEnvelope(obj: Envelope, pbf: any): void;

export interface Envelope_FieldEntry1 {
key?: string | null;
value?: string | null;
}

export function readEnvelope_FieldEntry1(pbf: any, end?: number): Envelope_FieldEntry1;
export function writeEnvelope_FieldEntry1(obj: Envelope_FieldEntry1, pbf: any): void;

export interface Envelope_FieldEntry2 {
key?: string | null;
value?: number | null;
}

export function readEnvelope_FieldEntry2(pbf: any, end?: number): Envelope_FieldEntry2;
export function writeEnvelope_FieldEntry2(obj: Envelope_FieldEntry2, pbf: any): void;
11 changes: 11 additions & 0 deletions test/fixtures/oneof.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

export interface Envelope {
id?: number | null;
int?: number | null;
float?: number | null;
string?: string | null;
value?: "int" | "float" | "string";
}

export function readEnvelope(pbf: any, end?: number): Envelope;
export function writeEnvelope(obj: Envelope, pbf: any): void;
24 changes: 24 additions & 0 deletions test/fixtures/packed.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

export interface NotPacked {
value: number[];
types: number[];
}

export function readNotPacked(pbf: any, end?: number): NotPacked;
export function writeNotPacked(obj: NotPacked, pbf: any): void;

export interface FalsePacked {
value: number[];
types: number[];
}

export function readFalsePacked(pbf: any, end?: number): FalsePacked;
export function writeFalsePacked(obj: FalsePacked, pbf: any): void;

export interface Packed {
value: number[];
types: number[];
}

export function readPacked(pbf: any, end?: number): Packed;
export function writePacked(obj: Packed, pbf: any): void;
Loading