Skip to content

Commit 6f98f2b

Browse files
committed
jsdoc support
1 parent a9d0a8d commit 6f98f2b

18 files changed

Lines changed: 813 additions & 25 deletions

bin/pbf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import resolve from 'resolve-protobuf-schema';
44
import {compileRaw} from '../compile.js';
55

66
if (process.argv.length < 3) {
7-
console.error('Usage: pbf [file.proto] [--no-read] [--no-write] [--legacy]');
7+
console.error('Usage: pbf [file.proto] [--no-read] [--no-write] [--jsdoc] [--legacy]');
88
process.exit(0);
99
}
1010

1111
const code = compileRaw(resolve.sync(process.argv[2]), {
1212
noRead: process.argv.indexOf('--no-read') >= 0,
1313
noWrite: process.argv.indexOf('--no-write') >= 0,
14+
jsDoc: process.argv.indexOf('--jsdoc') >= 0,
1415
legacy: process.argv.indexOf('--legacy') >= 0
1516
});
1617

compile.js

Lines changed: 138 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ export function compile(proto) {
99

1010
export function compileRaw(proto, options = {}) {
1111
const context = buildDefaults(buildContext(proto, null), proto.syntax);
12-
return `${options.dev ? '' : `// code generated by pbf v${version}\n`}${writeContext(context, options)}`;
12+
13+
let output = options.dev ? '' : `// code generated by pbf v${version}\n`;
14+
if (options.jsDoc) {
15+
output += typeDef(`import("${options.dev ? '../../index.js' : 'pbf'}").default`, 'Pbf');
16+
}
17+
output += writeContext(context, options);
18+
return output;
1319
}
1420

1521
function writeContext(ctx, options) {
@@ -24,17 +30,35 @@ function writeContext(ctx, options) {
2430
}
2531

2632
function writeMessage(ctx, options) {
33+
const name = ctx._name;
2734
const fields = ctx._proto.fields;
2835

2936
let code = '\n';
3037

38+
if (options.jsDoc) {
39+
code += compileType(ctx, name, fields);
40+
}
41+
3142
if (!options.noRead) {
32-
const readName = `read${ctx._name}`;
43+
const readName = `read${name}`;
44+
if (options.jsDoc) {
45+
code += ['\n/**', ' * @param {Pbf} pbf', ' * @param {number} [end]', ` * @returns {${name}}`, ' */'].join('\n').concat('\n');
46+
}
3347
code +=
3448
`${writeFunctionExport(options, readName)}function ${readName}(pbf, end) {
3549
return pbf.readFields(${readName}Field, ${compileDest(ctx)}, end);
36-
}
37-
function ${readName}Field(tag, obj, pbf) {
50+
}\n`;
51+
52+
if (options.jsDoc) {
53+
let param = name;
54+
if (ctx._proto.map) {
55+
const {key, value} = getMapTsType(fields);
56+
param = `{key: ${key}; value: ${value}}`;
57+
}
58+
code += ['\n/**', ' * @param {number} tag', ` * @param {${param}} obj`, ' * @param {Pbf} pbf', ' */'].join('\n').concat('\n');
59+
}
60+
code +=
61+
`function ${readName}Field(tag, obj, pbf) {
3862
`;
3963
for (let i = 0; i < fields.length; i++) {
4064
const field = fields[i];
@@ -60,7 +84,18 @@ function ${readName}Field(tag, obj, pbf) {
6084
}
6185

6286
if (!options.noWrite) {
63-
const writeName = `write${ctx._name}`;
87+
const writeName = `write${name}`;
88+
89+
if (options.jsDoc) {
90+
let param = name;
91+
if (ctx._proto.map) {
92+
const {key, value} = getMapTsType(fields);
93+
param = `{key: ${key}; value: ${value}}`;
94+
}
95+
96+
code += ['\n/**', ` * @param {${param}} obj`, ' * @param {Pbf} pbf', ' */'].join('\n').concat('\n');
97+
}
98+
6499
code += `${writeFunctionExport(options, writeName)}function ${writeName}(obj, pbf) {\n`;
65100
for (const field of fields) {
66101
const writeCode =
@@ -87,10 +122,17 @@ function getEnumValues(ctx) {
87122
return enums;
88123
}
89124

90-
function writeEnum(ctx, {legacy}) {
125+
function writeEnum(ctx, options) {
91126
const enums = JSON.stringify(getEnumValues(ctx), null, 4);
92127
const name = ctx._name;
93-
return `\n${legacy ? `const ${name} = exports.${name}` : `export const ${name}`} = ${enums};\n`;
128+
129+
let code = '\n';
130+
if (options.jsDoc) {
131+
code = '\n/** @enum {number} */\n';
132+
}
133+
134+
code += `${options.legacy ? `const ${name} = exports.${name}` : `export const ${name}`} = ${enums};\n`;
135+
return code;
94136
}
95137

96138
function compileDest(ctx) {
@@ -114,23 +156,27 @@ function getType(ctx, field) {
114156
return path.reduce((ctx, name) => ctx && ctx[name], ctx);
115157
}
116158

159+
function fieldTypeIsNumber(field) {
160+
switch (field.type) {
161+
case 'float':
162+
case 'double':
163+
case 'uint32':
164+
case 'uint64':
165+
case 'int32':
166+
case 'int64':
167+
case 'sint32':
168+
case 'sint64':
169+
case 'fixed32':
170+
case 'fixed64':
171+
case 'sfixed32':
172+
case 'sfixed64': return true;
173+
default: return false;
174+
}
175+
}
176+
117177
function fieldShouldUseStringAsNumber(field) {
118178
if (field.options.jstype === 'JS_STRING') {
119-
switch (field.type) {
120-
case 'float':
121-
case 'double':
122-
case 'uint32':
123-
case 'uint64':
124-
case 'int32':
125-
case 'int64':
126-
case 'sint32':
127-
case 'sint64':
128-
case 'fixed32':
129-
case 'fixed64':
130-
case 'sfixed32':
131-
case 'sfixed64': return true;
132-
default: return false;
133-
}
179+
return fieldTypeIsNumber(field);
134180
}
135181
return false;
136182
}
@@ -256,6 +302,7 @@ function getMapMessage(field) {
256302
return {
257303
name: getMapMessageName(field.tag),
258304
enums: [],
305+
map: true,
259306
messages: [],
260307
extensions: null,
261308
fields: [
@@ -415,3 +462,72 @@ function getDefaultWriteTest(ctx, field) {
415462
function isPacked(field) {
416463
return field.options.packed === 'true';
417464
}
465+
466+
function getTsType(field) {
467+
let type = field.type;
468+
469+
if (fieldShouldUseStringAsNumber(field)) type = 'string';
470+
else if (fieldTypeIsNumber(field)) type = 'number';
471+
else if (field.type === 'bytes') type = 'Uint8Array';
472+
else if (field.type === 'bool') type = 'boolean';
473+
474+
return field.repeated ? `Array<${type}>` : type;
475+
}
476+
477+
function getMapTsType(fields) {
478+
const key = getTsType(fields[0]);
479+
const value = getTsType(fields[1]);
480+
return {key, value};
481+
}
482+
483+
/**
484+
* @param {string} type
485+
* @param {string} name
486+
* @param {{name: string; type: string; required: boolean}} [fields]
487+
* @returns {string}
488+
*/
489+
function typeDef(type, name, fields = []) {
490+
const unionProperties = {};
491+
const properties = fields.map((field) => {
492+
if (field.oneof) {
493+
unionProperties[field.oneof] = unionProperties[field.oneof] || [];
494+
unionProperties[field.oneof].push(field.name);
495+
}
496+
497+
const type = getTsType(field);
498+
const isRequired = field.required || field.repeated || field.map;
499+
500+
const name = isRequired ? field.name : `[${field.name}]`;
501+
502+
return ` * @property {${type}} ${name}`;
503+
});
504+
505+
for (const prop in unionProperties) {
506+
const union = unionProperties[prop].map(s => `"${s}"`).join(' | ');
507+
properties.push(` * @property {${union}} [${prop}]`);
508+
}
509+
510+
return ['/**', ` * @typedef {${type}} ${name}`, ...properties, ' */']
511+
.join('\n')
512+
.concat('\n');
513+
}
514+
515+
/**
516+
* @param {object} ctx
517+
* @param {string} name
518+
* @param {{name: string; type: string; required: boolean}} [fields]
519+
* @returns {string}
520+
*/
521+
function compileType(ctx, name, fields = []) {
522+
if (ctx._proto.map) {
523+
const {key, value} = getMapTsType(fields);
524+
return typeDef(`Object<${key}, ${value}>`, name, []);
525+
}
526+
527+
const typedFields = fields.map((field) => {
528+
const type = getType(ctx, field);
529+
return {...field, type: type ? type._name : field.type};
530+
});
531+
532+
return typeDef('object', name, typedFields);
533+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"scripts": {
1212
"bench": "node bench/bench.js",
1313
"pretest": "eslint *.js compile.js test/*.js test/fixtures/*.js bin/pbf",
14-
"test": "tsc && node --test",
14+
"test": "tsc && node --test && tsc --project tsconfig.test.json",
1515
"cov": "node --test --experimental-test-covetage",
1616
"build": "rollup -c",
1717
"prepublishOnly": "npm run test && npm run build"

test/compile.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ test('compiles all proto files to proper js', () => {
1212
for (const path of files) {
1313
if (!path.endsWith('.proto')) continue;
1414
const proto = resolve(new URL(`fixtures/${path}`, import.meta.url));
15-
const js = compileRaw(proto, {dev: true});
15+
const js = compileRaw(proto, {dev: true, jsDoc: true});
1616

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

test/fixtures/defaults.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,48 @@
1+
/**
2+
* @typedef {import("../../index.js").default} Pbf
3+
*/
14

5+
/** @enum {number} */
26
export const MessageType = {
37
"UNKNOWN": 0,
48
"GREETING": 1
59
};
610

11+
/**
12+
* @typedef {object} Envelope
13+
* @property {MessageType} [type]
14+
* @property {string} [name]
15+
* @property {boolean} [flag]
16+
* @property {number} [weight]
17+
* @property {number} [id]
18+
*/
19+
20+
/**
21+
* @param {Pbf} pbf
22+
* @param {number} [end]
23+
* @returns {Envelope}
24+
*/
725
export function readEnvelope(pbf, end) {
826
return pbf.readFields(readEnvelopeField, {type: 1, name: "test", flag: true, weight: 1.5, id: 1}, end);
927
}
28+
29+
/**
30+
* @param {number} tag
31+
* @param {Envelope} obj
32+
* @param {Pbf} pbf
33+
*/
1034
function readEnvelopeField(tag, obj, pbf) {
1135
if (tag === 1) obj.type = pbf.readVarint();
1236
else if (tag === 2) obj.name = pbf.readString();
1337
else if (tag === 3) obj.flag = pbf.readBoolean();
1438
else if (tag === 4) obj.weight = pbf.readFloat();
1539
else if (tag === 5) obj.id = pbf.readVarint(true);
1640
}
41+
42+
/**
43+
* @param {Envelope} obj
44+
* @param {Pbf} pbf
45+
*/
1746
export function writeEnvelope(obj, pbf) {
1847
if (obj.type != null && obj.type !== 1) pbf.writeVarintField(1, obj.type);
1948
if (obj.name != null && obj.name !== "test") pbf.writeStringField(2, obj.name);

test/fixtures/defaults_implicit.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,69 @@
1+
/**
2+
* @typedef {import("../../index.js").default} Pbf
3+
*/
14

5+
/** @enum {number} */
26
export const MessageType = {
37
"UNKNOWN": 0,
48
"GREETING": 1
59
};
610

11+
/**
12+
* @typedef {object} CustomType
13+
*/
14+
15+
/**
16+
* @param {Pbf} pbf
17+
* @param {number} [end]
18+
* @returns {CustomType}
19+
*/
720
export function readCustomType(pbf, end) {
821
return pbf.readFields(readCustomTypeField, {}, end);
922
}
23+
24+
/**
25+
* @param {number} tag
26+
* @param {CustomType} obj
27+
* @param {Pbf} pbf
28+
*/
1029
function readCustomTypeField(tag, obj, pbf) {
1130
}
31+
32+
/**
33+
* @param {CustomType} obj
34+
* @param {Pbf} pbf
35+
*/
1236
export function writeCustomType(obj, pbf) {
1337
}
1438

39+
/**
40+
* @typedef {object} Envelope
41+
* @property {MessageType} [type]
42+
* @property {string} [name]
43+
* @property {boolean} [flag]
44+
* @property {number} [weight]
45+
* @property {number} [id]
46+
* @property {Array<string>} tags
47+
* @property {Array<number>} numbers
48+
* @property {Uint8Array} [bytes]
49+
* @property {CustomType} [custom]
50+
* @property {Array<MessageType>} types
51+
*/
52+
53+
/**
54+
* @param {Pbf} pbf
55+
* @param {number} [end]
56+
* @returns {Envelope}
57+
*/
1558
export function readEnvelope(pbf, end) {
1659
return pbf.readFields(readEnvelopeField, {type: 0, name: "", flag: false, weight: 0, id: 0, tags: [], numbers: [], bytes: undefined, custom: undefined, types: []}, end);
1760
}
61+
62+
/**
63+
* @param {number} tag
64+
* @param {Envelope} obj
65+
* @param {Pbf} pbf
66+
*/
1867
function readEnvelopeField(tag, obj, pbf) {
1968
if (tag === 1) obj.type = pbf.readVarint();
2069
else if (tag === 2) obj.name = pbf.readString();
@@ -27,6 +76,11 @@ function readEnvelopeField(tag, obj, pbf) {
2776
else if (tag === 9) obj.custom = readCustomType(pbf, pbf.readVarint() + pbf.pos);
2877
else if (tag === 10) pbf.readPackedVarint(obj.types);
2978
}
79+
80+
/**
81+
* @param {Envelope} obj
82+
* @param {Pbf} pbf
83+
*/
3084
export function writeEnvelope(obj, pbf) {
3185
if (obj.type) pbf.writeVarintField(1, obj.type);
3286
if (obj.name) pbf.writeStringField(2, obj.name);

0 commit comments

Comments
 (0)