@mercari/proto-to-type v1.1.1
proto-to-type
proto-to-type is almost pure Node.js library that generates type definition for TypeScript from Protocol Buffer file(.proto).
This library uses some packages from npm registry, protobufjs is used as .proto file parser, array-flatten helps serialization and prettier is used as default formatter.
It also uses hub command to fetch .proto files from GitHub.
Installation
$ npm i -D proto-to-type
$ npm i -D prettier # You can skip if you use custom formatter.hub command
You can follow this page to install it. (You can skip if you have already had the command.)
Usage
Sample Code
import ProtoToType, { ImportResolver } from 'proto-to-type';
import { writeFileSync } from 'fs';
const locals = ['entrypoint.proto', 'path/to/another.proto'];
const resolveImport: ImportResolver = (target) => {
if (locals.includes(target)) {
return ['read-file', 'path/to/proto/directory/' + target];
} else if (target.startsWith('google/api/')) {
return ['github-contents', 'googleapis/googleapis', target, 'master'];
} else if (target.startsWith('google/protobuf/')) {
return ['github-contents', 'protocolbuffers/protobuf', `src/${target}`, '176f7db1...'/* full of commit id */];
}
return null;
};
const generate = ProtoToType({
resolveImport,
predefinedTypes: { string: 'string', int32: 'number', 'other.custom.types': 'CustomTypeYouWant' },
});
generate('CustomNamespace', 'entrypoint.proto', ['Entrypoint']).then((result) => {
writeFileSync('path/to/out.d.ts', result);
});ProtoToType
It initializes the generator. You can set options, some are required.
All Generator Options
| name | type | description |
|---|---|---|
resolveImport | function | required |
predefinedTypes | object | required |
fetchers | object | custom fetchers |
transformWrapper | function | use to inject custom lines |
format | function | custom formatter (use prettier if omitted) |
enumFormat | 'str'(default), 'num', 'mixed' | how to define enums |
strictOneof | false(default), true, 'hybrid' | whether apply oneof strictly |
generator
The generator requires three arguments.
| index | type | description |
|:----- |:---- |:----------- |
| 0 | string | name of top namespace that stores output declarations |
| 1 | string | file target of entry .proto file (Note that set it as a non-resolved target, see the importResolver section below)
| 2 | string[] | entry items in the .proto file, such as services and messages |
importResolver
You need to implement importResolver and tell fetcher about location information of .proto files.
importResolver must return ProtoRequest or null. ProtoRequest is a string array type and requires one string at least. First string specifies fetcher type(described later) and the rest items are passed as arguments to picked fetcher function.
If the resolver returns null, the target is skipped to load.
type ImportResolver = (target: string, weak: boolean) => ProtoRequest | null;
type ProtoRequest = [string /* fetcher type */, ...string[] /* arguments for fetcher */];fetcher
proto-to-type supports two fetchers read-file and github-contents by default.
read-file
It uses fs.readFileSync, accepts only one argument(from the rest items) as the file path of .proto file.
github-contents
It uses hub command via child process. You may need set hub command up to access private repositories.
| index | description |
|---|---|
| 0 | repository name (ex: 'protocolbuffers/protobuf') |
| 1 | path to .proto file |
| 2 | commit, branch, tag id (ex: 'master') |
fetchers (Custom Fetcher)
You can add your custom fetcher.
Note that you can overwrite the existing two fetchers if you use the same key.
const resolveImport: ImportResolver = (target) => {
return ['custom-fetcher-name', target, 'other', 'arguments', 'you', 'need'];
};
const generate = ProtoToType({
resolveImport,
fetchers: {
// you can skip arguments type declaration
'custom-fetcher-name': (target, other, arguments, I, want) => {
/* ...implementation */
return ''; // return `.proto` file content as a string
},
},
/* ...other options */
});predefinedTypes
Please don't forget filling predefinedTypes. By default, a primitive type (int32, string, bool) is serialized as it is.
If you skip loading some .proto file in resolveImport, types may not be resolved by the parser and be serialized as written in .proto file.
const generate = ProtoToType({
resolveImport,
predefinedTypes: { string: 'string', int32: 'number', 'other.custom.types': 'CustomTypeYouWant' },
});transformWrapper
Set transformWrapper in generator options if you want to insert custom lines, such as a comment to show that the file is generated one. It receives a tuple [string, string] and you should return in the same format, the first is the top content and the second is the bottom.
Note that thay are not empty strings, you can replace the content but it may break results.
const generate = ProtoToType({
resolveImport,
predefinedTypes: {},
transformWrapper: ([header, footer]) => [`// DO NOT EDIT - GENERATED FILE\n${header}`, footer],
});enumFormat
https://developers.google.com/protocol-buffers/docs/proto3#json_options
The name of the enum value as specified in proto is used. Parsers accept both enum names and integer values.
Enum is returned as a string by default, but you can send a request with numeric enum value too. And backend API you use may return enums as a number. So proto-to-type supports both cases. See the examples below. You can use 'str', 'num' and 'mixed'.
input .proto file
enum Sample {
UNKNOWN = 0;
ACTIVE = 1;
INACTIVE = 2;
NOT_FOUND = 404;
}output .d.ts files
enumFormat: 'str'(default)
type Sample =
| Sample.UNKNOWN
| Sample.ACTIVE
| Sample.INACTIVE
| Sample.NOT_FOUND;
namespace Sample {
type UNKNOWN = "UNKNOWN";
namespace UNKNOWN {
const str: "UNKNOWN";
const num: 0;
}
type ACTIVE = "ACTIVE";
namespace ACTIVE {
const str: "ACTIVE";
const num: 1;
}
type INACTIVE = "INACTIVE";
namespace INACTIVE {
const str: "INACTIVE";
const num: 2;
}
type NOT_FOUND = "NOT_FOUND";
namespace NOT_FOUND {
const str: "NOT_FOUND";
const num: 404;
}
}enumFormat: 'num'
type Sample =
| Sample.UNKNOWN
| Sample.ACTIVE
| Sample.INACTIVE
| Sample.NOT_FOUND;
namespace Sample {
type UNKNOWN = 0;
namespace UNKNOWN {
const str: "UNKNOWN";
const num: 0;
}
type ACTIVE = 1;
namespace ACTIVE {
const str: "ACTIVE";
const num: 1;
}
type INACTIVE = 2;
namespace INACTIVE {
const str: "INACTIVE";
const num: 2;
}
type NOT_FOUND = 404;
namespace NOT_FOUND {
const str: "NOT_FOUND";
const num: 404;
}
}enumFormat: 'mixed'
type Sample =
| Sample.UNKNOWN
| Sample.ACTIVE
| Sample.INACTIVE
| Sample.NOT_FOUND;
namespace Sample {
type UNKNOWN = "UNKNOWN" | 0;
namespace UNKNOWN {
const str: "UNKNOWN";
const num: 0;
}
type ACTIVE = "ACTIVE" | 1;
namespace ACTIVE {
const str: "ACTIVE";
const num: 1;
}
type INACTIVE = "INACTIVE" | 2;
namespace INACTIVE {
const str: "INACTIVE";
const num: 2;
}
type NOT_FOUND = "NOT_FOUND" | 404;
namespace NOT_FOUND {
const str: "NOT_FOUND";
const num: 404;
}
}strictOneof
proto-to-type supports protobuf's oneof type strictly, but the output may be complex for you, so proto-to-type generate non-strict oneof by default. You can make it srtict as you need.
Package Release Flow
We use semantic-release to automate package release workflow.
- Note that new version will be published every time when anything is merged into master.
- Merge to
nextbranch first if you don't want to set the version as default. Users can install the version bynpm i @mercari/proto-to-type@next. - Create branches such as
1.x.xor1.1.xif you want to maintain old major releases.
Take a look this doc about more detail.
Committers
- Shota Hatada (@whatasoda)
Contribution
Please read the CLA carefully before submitting your contribution to Mercari. Under any circumstances, by submitting your contribution, you are deemed to accept and agree to be bound by the terms and conditions of the CLA.
License
Copyright 2020 Mercari, Inc.
Licensed under the MIT License.