@bufbuild/protoc-gen-es
Version:
Protocol Buffers code generator for ECMAScript
508 lines (507 loc) • 22.4 kB
JavaScript
"use strict";
// Copyright 2021-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.protocGenEs = void 0;
const reflect_1 = require("@bufbuild/protobuf/reflect");
const codegenv2_1 = require("@bufbuild/protobuf/codegenv2");
const wkt_1 = require("@bufbuild/protobuf/wkt");
const protoplugin_1 = require("@bufbuild/protoplugin");
const util_js_1 = require("./util.js");
const package_json_1 = require("../package.json");
const valid_types_js_1 = require("./valid-types.js");
exports.protocGenEs = (0, protoplugin_1.createEcmaScriptPlugin)({
name: "protoc-gen-es",
version: `v${String(package_json_1.version)}`,
parseOptions,
generateTs,
generateJs,
generateDts,
});
function parseOptions(options) {
let jsonTypes = false;
let validTypes = {
legacyRequired: false,
protovalidateRequired: false,
};
for (const { key, value } of options) {
switch (key) {
case "json_types":
if (!["true", "1", "false", "0"].includes(value)) {
throw "please provide true or false";
}
jsonTypes = ["true", "1"].includes(value);
break;
case "valid_types":
for (const part of value.split("+")) {
switch (part) {
case "protovalidate_required":
validTypes.protovalidateRequired = true;
break;
case "legacy_required":
validTypes.legacyRequired = true;
break;
default:
throw new Error();
}
}
break;
default:
throw new Error();
}
}
return { jsonTypes, validTypes };
}
// This annotation informs bundlers that the succeeding function call is free of
// side effects. This means the symbol can be removed from the module during
// tree-shaking if it is unused.
// See https://github.com/bufbuild/protobuf-es/pull/470
const pure = "/*@__PURE__*/";
// biome-ignore format: want this to read well
function generateTs(schema) {
for (const file of schema.files) {
const f = schema.generateFile(file.name + "_pb.ts");
f.preamble(file);
const { GenFile } = f.runtime.codegen;
const fileDesc = f.importSchema(file);
generateDescDoc(f, file);
f.print(f.export("const", fileDesc.name), ": ", GenFile, " = ", pure);
f.print(" ", getFileDescCall(f, file, schema), ";");
f.print();
for (const desc of schema.typesInFile(file)) {
switch (desc.kind) {
case "message": {
generateMessageShape(f, desc, "ts");
if (schema.options.jsonTypes) {
generateMessageJsonShape(f, desc, "ts");
}
if (schema.options.validTypes.legacyRequired || schema.options.validTypes.protovalidateRequired) {
generateMessageValidShape(f, desc, schema.options.validTypes, "ts");
}
generateDescDoc(f, desc);
const name = f.importSchema(desc).name;
f.print(f.export("const", name), ": ", (0, util_js_1.messageGenType)(desc, f, schema.options), " = ", pure);
const call = (0, util_js_1.functionCall)(f.runtime.codegen.messageDesc, [fileDesc, ...(0, codegenv2_1.pathInFileDesc)(desc)]);
f.print(" ", call, ";");
f.print();
break;
}
case "enum": {
generateEnumShape(f, desc);
if (schema.options.jsonTypes) {
generateEnumJsonShape(f, desc, "ts");
}
generateDescDoc(f, desc);
const name = f.importSchema(desc).name;
const Shape = f.importShape(desc);
const { GenEnum, enumDesc } = f.runtime.codegen;
if (schema.options.jsonTypes) {
const JsonType = f.importJson(desc);
f.print(f.export("const", name), ": ", GenEnum, "<", Shape, ", ", JsonType, ">", " = ", pure);
}
else {
f.print(f.export("const", name), ": ", GenEnum, "<", Shape, ">", " = ", pure);
}
const call = (0, util_js_1.functionCall)(enumDesc, [fileDesc, ...(0, codegenv2_1.pathInFileDesc)(desc)]);
f.print(" ", call, ";");
f.print();
break;
}
case "extension": {
const { GenExtension, extDesc } = f.runtime.codegen;
const name = f.importSchema(desc).name;
const E = f.importShape(desc.extendee);
const V = (0, util_js_1.fieldTypeScriptType)(desc, f.runtime).typing;
const call = (0, util_js_1.functionCall)(extDesc, [fileDesc, ...(0, codegenv2_1.pathInFileDesc)(desc)]);
f.print(f.jsDoc(desc));
f.print(f.export("const", name), ": ", GenExtension, "<", E, ", ", V, ">", " = ", pure);
f.print(" ", call, ";");
f.print();
break;
}
case "service": {
const { GenService, serviceDesc } = f.runtime.codegen;
const name = f.importSchema(desc).name;
const call = (0, util_js_1.functionCall)(serviceDesc, [fileDesc, ...(0, codegenv2_1.pathInFileDesc)(desc)]);
f.print(f.jsDoc(desc));
f.print(f.export("const", name), ": ", GenService, "<", getServiceShapeExpr(f, desc), "> = ", pure);
f.print(" ", call, ";");
f.print();
break;
}
}
}
}
}
// biome-ignore format: want this to read well
function generateJs(schema) {
for (const file of schema.files) {
const f = schema.generateFile(file.name + "_pb.js");
f.preamble(file);
const fileDesc = f.importSchema(file);
generateDescDoc(f, file);
f.print(f.export("const", fileDesc.name), " = ", pure);
f.print(" ", getFileDescCall(f, file, schema), ";");
f.print();
for (const desc of schema.typesInFile(file)) {
switch (desc.kind) {
case "message": {
const name = f.importSchema(desc).name;
generateDescDoc(f, desc);
const { messageDesc } = f.runtime.codegen;
const call = (0, util_js_1.functionCall)(messageDesc, [fileDesc, ...(0, codegenv2_1.pathInFileDesc)(desc)]);
f.print(f.export("const", name), " = ", pure);
f.print(" ", call, ";");
f.print();
break;
}
case "enum": {
// generate descriptor
{
generateDescDoc(f, desc);
const name = f.importSchema(desc).name;
f.print(f.export("const", name), " = ", pure);
const { enumDesc } = f.runtime.codegen;
const call = (0, util_js_1.functionCall)(enumDesc, [fileDesc, ...(0, codegenv2_1.pathInFileDesc)(desc)]);
f.print(" ", call, ";");
f.print();
}
// declare TypeScript enum
{
f.print(f.jsDoc(desc));
f.print(f.export("const", f.importShape(desc).name), " = ", pure);
const { tsEnum } = f.runtime.codegen;
const call = (0, util_js_1.functionCall)(tsEnum, [f.importSchema(desc)]);
f.print(" ", call, ";");
f.print();
}
break;
}
case "extension": {
f.print(f.jsDoc(desc));
const name = f.importSchema(desc).name;
f.print(f.export("const", name), " = ", pure);
const { extDesc } = f.runtime.codegen;
const call = (0, util_js_1.functionCall)(extDesc, [fileDesc, ...(0, codegenv2_1.pathInFileDesc)(desc)]);
f.print(" ", call, ";");
f.print();
break;
}
case "service": {
f.print(f.jsDoc(desc));
const name = f.importSchema(desc).name;
f.print(f.export("const", name), " = ", pure);
const { serviceDesc } = f.runtime.codegen;
const call = (0, util_js_1.functionCall)(serviceDesc, [fileDesc, ...(0, codegenv2_1.pathInFileDesc)(desc)]);
f.print(" ", call, ";");
f.print();
break;
}
}
}
}
}
// biome-ignore format: want this to read well
function generateDts(schema) {
for (const file of schema.files) {
const f = schema.generateFile(file.name + "_pb.d.ts");
f.preamble(file);
const { GenFile } = f.runtime.codegen;
const fileDesc = f.importSchema(file);
generateDescDoc(f, file);
f.print(f.export("declare const", fileDesc.name), ": ", GenFile, ";");
f.print();
for (const desc of schema.typesInFile(file)) {
switch (desc.kind) {
case "message": {
generateMessageShape(f, desc, "dts");
if (schema.options.jsonTypes) {
generateMessageJsonShape(f, desc, "dts");
}
if (schema.options.validTypes.legacyRequired || schema.options.validTypes.protovalidateRequired) {
generateMessageValidShape(f, desc, schema.options.validTypes, "dts");
}
const name = f.importSchema(desc).name;
generateDescDoc(f, desc);
f.print(f.export("declare const", name), ": ", (0, util_js_1.messageGenType)(desc, f, schema.options), ";");
f.print();
break;
}
case "enum": {
generateEnumShape(f, desc);
if (schema.options.jsonTypes) {
generateEnumJsonShape(f, desc, "dts");
}
generateDescDoc(f, desc);
const name = f.importSchema(desc).name;
const Shape = f.importShape(desc);
const { GenEnum } = f.runtime.codegen;
if (schema.options.jsonTypes) {
const JsonType = f.importJson(desc);
f.print(f.export("declare const", name), ": ", GenEnum, "<", Shape, ", ", JsonType, ">;");
}
else {
f.print(f.export("declare const", name), ": ", GenEnum, "<", Shape, ">;");
}
f.print();
break;
}
case "extension": {
const { GenExtension } = f.runtime.codegen;
const name = f.importSchema(desc).name;
const E = f.importShape(desc.extendee);
const V = (0, util_js_1.fieldTypeScriptType)(desc, f.runtime).typing;
f.print(f.jsDoc(desc));
f.print(f.export("declare const", name), ": ", GenExtension, "<", E, ", ", V, ">;");
f.print();
break;
}
case "service": {
const { GenService } = f.runtime.codegen;
const name = f.importSchema(desc).name;
f.print(f.jsDoc(desc));
f.print(f.export("declare const", name), ": ", GenService, "<", getServiceShapeExpr(f, desc), ">;");
f.print();
break;
}
}
}
}
}
function generateDescDoc(f, desc) {
let lines;
switch (desc.kind) {
case "file":
lines = [`Describes the ${desc.toString()}.`];
break;
case "message":
lines = [
`Describes the ${desc.toString()}.`,
`Use \`create(${f.importSchema(desc).name})\` to create a new message.`,
];
break;
case "enum":
lines = [`Describes the ${desc.toString()}.`];
break;
}
const deprecated = desc.deprecated || (0, reflect_1.parentTypes)(desc).some((d) => d.deprecated);
if (deprecated) {
lines.push("@deprecated");
}
f.print({
kind: "es_jsdoc",
text: lines.join("\n"),
});
}
// biome-ignore format: want this to read well
function getFileDescCall(f, file, schema) {
// Schema provides files with source retention options. Since we do not want to
// embed source retention options in generated code, we use FileDescriptorProto
// messages from CodeGeneratorRequest.proto_file instead.
const sourceFile = file.proto;
const runtimeFile = schema.proto.protoFile.find(f => f.name == sourceFile.name);
const info = (0, codegenv2_1.embedFileDesc)(runtimeFile !== null && runtimeFile !== void 0 ? runtimeFile : sourceFile);
if (info.bootable && !f.runtime.create.from.startsWith("@bufbuild/protobuf")) {
// google/protobuf/descriptor.proto is embedded as a plain object when
// bootstrapping to avoid recursion
return (0, util_js_1.functionCall)(f.runtime.codegen.boot, [JSON.stringify(info.boot())]);
}
const { fileDesc } = f.runtime.codegen;
if (file.dependencies.length > 0) {
const deps = file.dependencies.map((f) => ({
kind: "es_desc_ref",
desc: f,
}));
return (0, util_js_1.functionCall)(fileDesc, [
f.string(info.base64()),
f.array(deps),
]);
}
return (0, util_js_1.functionCall)(fileDesc, [f.string(info.base64())]);
}
// biome-ignore format: want this to read well
function getServiceShapeExpr(f, service) {
const p = ["{\n"];
for (const method of service.methods) {
p.push(f.jsDoc(method, " "), "\n");
p.push(" ", method.localName, ": {\n");
p.push(" methodKind: ", f.string(method.methodKind), ";\n");
p.push(" input: typeof ", f.importSchema(method.input, true), ";\n");
p.push(" output: typeof ", f.importSchema(method.output, true), ";\n");
p.push(" },\n");
}
p.push("}");
return p;
}
// biome-ignore format: want this to read well
function generateEnumShape(f, enumeration) {
f.print(f.jsDoc(enumeration));
f.print(f.export("enum", f.importShape(enumeration).name), " {");
for (const value of enumeration.values) {
if (enumeration.values.indexOf(value) > 0) {
f.print();
}
f.print(f.jsDoc(value, " "));
f.print(" ", value.localName, " = ", value.number, ",");
}
f.print("}");
f.print();
}
// biome-ignore format: want this to read well
function generateEnumJsonShape(f, enumeration, target) {
f.print(f.jsDoc(enumeration));
const declaration = target == "ts" ? "type" : "declare type";
const values = [];
if (enumeration.typeName == "google.protobuf.NullValue") {
values.push("null");
}
else {
for (const v of enumeration.values) {
if (enumeration.values.indexOf(v) > 0) {
values.push(" | ");
}
values.push(f.string(v.name));
}
}
f.print(f.export(declaration, f.importJson(enumeration).name), " = ", values, ";");
f.print();
}
// biome-ignore format: want this to read well
function generateMessageShape(f, message, target) {
const { Message } = f.runtime;
const declaration = target == "ts" ? "type" : "declare type";
f.print(f.jsDoc(message));
f.print(f.export(declaration, f.importShape(message).name), " = ", Message, "<", f.string(message.typeName), "> & {");
for (const member of message.members) {
generateMessageShapeMember(f, member);
if (message.members.indexOf(member) < message.members.length - 1) {
f.print();
}
}
f.print("};");
f.print();
}
// biome-ignore format: want this to read well
function generateMessageValidShape(f, message, validTypes, target) {
const declaration = target == "ts" ? "type" : "declare type";
const needsCustomValidType = (validTypes.legacyRequired && message.fields.some(valid_types_js_1.isLegacyRequired)) || (validTypes.protovalidateRequired && message.fields.some(valid_types_js_1.isProtovalidateRequired));
if (!needsCustomValidType) {
f.print(f.export(declaration, f.importValid(message).name), " = ", f.importShape(message), ";");
f.print();
return;
}
f.print(f.jsDoc(message));
const { Message } = f.runtime;
f.print(f.export(declaration, f.importValid(message).name), " = ", Message, "<", f.string(message.typeName), "> & {");
for (const member of message.members) {
generateMessageShapeMember(f, member, validTypes);
if (message.members.indexOf(member) < message.members.length - 1) {
f.print();
}
}
f.print("};");
f.print();
}
// biome-ignore format: want this to read well
function generateMessageShapeMember(f, member, validTypes) {
switch (member.kind) {
case "oneof":
f.print(f.jsDoc(member, " "));
f.print(" ", member.localName, ": {");
for (const field of member.fields) {
if (member.fields.indexOf(field) > 0) {
f.print(` } | {`);
}
f.print(f.jsDoc(field, " "));
const { typing } = (0, util_js_1.fieldTypeScriptType)(field, f.runtime, validTypes && !(0, valid_types_js_1.isProtovalidateDisabled)(field));
f.print(` value: `, typing, `;`);
f.print(` case: "`, field.localName, `";`);
}
f.print(` } | { case: undefined; value?: undefined };`);
break;
case "field":
f.print(f.jsDoc(member, " "));
let { typing, optional } = (0, util_js_1.fieldTypeScriptType)(member, f.runtime, validTypes && !(0, valid_types_js_1.isProtovalidateDisabled)(member));
if (optional && validTypes) {
const isRequired = (validTypes.legacyRequired && (0, valid_types_js_1.isLegacyRequired)(member)) || (validTypes.protovalidateRequired && (0, valid_types_js_1.isProtovalidateRequired)(member));
if (isRequired) {
optional = false;
}
}
if (optional) {
f.print(" ", member.localName, "?: ", typing, ";");
}
else {
f.print(" ", member.localName, ": ", typing, ";");
}
break;
}
}
// biome-ignore format: want this to read well
function generateMessageJsonShape(f, message, target) {
const exp = f.export(target == "ts" ? "type" : "declare type", f.importJson(message).name);
f.print(f.jsDoc(message));
switch (message.typeName) {
case "google.protobuf.Any":
f.print(exp, " = {");
f.print(` "@type"?: string;`);
f.print("};");
break;
case "google.protobuf.Timestamp":
f.print(exp, " = string;");
break;
case "google.protobuf.Duration":
f.print(exp, " = string;");
break;
case "google.protobuf.FieldMask":
f.print(exp, " = string;");
break;
case "google.protobuf.Struct":
f.print(exp, " = ", f.runtime.JsonObject, ";");
break;
case "google.protobuf.Value":
f.print(exp, " = ", f.runtime.JsonValue, ";");
break;
case "google.protobuf.ListValue":
f.print(exp, " = ", f.runtime.JsonValue, "[];");
break;
case "google.protobuf.Empty":
f.print(exp, " = Record<string, never>;");
break;
default:
if ((0, wkt_1.isWrapperDesc)(message)) {
f.print(exp, " = ", (0, util_js_1.fieldJsonType)(message.fields[0]), ";");
}
else {
f.print(exp, " = {");
for (const field of message.fields) {
f.print(f.jsDoc(field, " "));
let jsonName = field.jsonName;
const startWithNumber = /^[0-9]/;
const containsSpecialChar = /[^a-zA-Z0-9_$]/;
if (jsonName === ""
|| startWithNumber.test(jsonName)
|| containsSpecialChar.test(jsonName)) {
jsonName = f.string(jsonName);
}
f.print(" ", jsonName, "?: ", (0, util_js_1.fieldJsonType)(field), ";");
if (message.fields.indexOf(field) < message.fields.length - 1) {
f.print();
}
}
f.print("};");
}
}
f.print();
}