@bufbuild/protoplugin
Version:
Helps to create your own Protocol Buffers code generators.
221 lines (220 loc) • 7.55 kB
JavaScript
// 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.
import { wktPublicImportPaths } from "@bufbuild/protobuf/codegenv2";
import { nestedTypes } from "@bufbuild/protobuf/reflect";
import { safeIdentifier } from "./safe-identifier.js";
/**
* Return a file path for the give file descriptor.
*/
export function generateFilePath(file, bootstrapWkt, filesToGenerate) {
// Well-known types are published with the runtime package. We usually want
// the generated code to import them from the runtime package, with the
// following exceptions:
// 1. We are bootstrapping the runtime package via the plugin option
// "bootstrap_wkt". In that case, we cannot refer to the runtime package
// itself.
// 2. We were explicitly asked to generate the well-known type.
const wktFrom = wktPublicImportPaths[file.proto.name];
if (wktFrom !== undefined &&
!bootstrapWkt &&
!filesToGenerate.find((f) => f.name === file.name)) {
return wktFrom;
}
return "./" + file.name + "_pb.js";
}
/**
* Return a safe identifier for a generated descriptor.
*/
export function generatedDescName(desc) {
const file = desc.kind == "file" ? desc : desc.file;
const { descNames } = allNames(file);
const name = descNames.get(desc);
if (name === undefined) {
throw new Error(`unable to determine unique descriptor name for ${desc.toString()}`);
}
return name;
}
/**
* Return a safe identifier for a generated shape.
*/
export function generatedShapeName(desc) {
const { shapeNames } = allNames(desc.file);
const name = shapeNames.get(desc);
if (name === undefined) {
throw new Error(`unable to determine unique shape name for ${desc.toString()}`);
}
return name;
}
/**
* Return a safe identifier for a generated JSON type.
*/
export function generatedJsonTypeName(desc) {
const { jsonTypeNames } = allNames(desc.file);
const name = jsonTypeNames.get(desc);
if (name === undefined) {
throw new Error(`unable to determine unique json type name for ${desc.toString()}`);
}
return name;
}
/**
* Return a safe identifier for a generated Valid type.
*/
export function generatedValidTypeName(desc) {
const { validTypeNames } = allNames(desc.file);
const name = validTypeNames.get(desc);
if (name === undefined) {
throw new Error(`unable to determine unique valid type name for ${desc.toString()}`);
}
return name;
}
/**
* Compute the ideal name for a generated descriptor.
*/
function idealDescName(desc, i) {
const salt = i === 0 ? "" : i === 1 ? "$" : `$${i - 1}`;
if (desc.kind == "file") {
const name = "file_" + desc.name.replace(/[^a-zA-Z0-9_]+/g, "_");
return safeIdentifier(name + salt);
}
switch (desc.kind) {
case "enum":
return safeIdentifier(identifier(desc) + "Schema" + salt);
case "message":
return safeIdentifier(identifier(desc) + "Schema" + salt);
case "extension":
return safeIdentifier(identifier(desc) + salt);
case "service":
return safeIdentifier(identifier(desc) + salt);
}
}
/**
* Compute the ideal name for a generated shape.
*/
function idealShapeName(desc, i) {
const salt = i === 0 ? "" : i === 1 ? "$" : `$${i - 1}`;
return safeIdentifier(identifier(desc) + salt);
}
/**
* Compute the ideal name for a generated JSON type.
*/
function idealJsonTypeName(desc, i) {
const salt = i === 0 ? "" : i === 1 ? "$" : `$${i - 1}`;
return safeIdentifier(identifier(desc) + "Json" + salt);
}
/**
* Compute the ideal name for a generated Valid type.
*/
function idealValidTypeName(desc, i) {
const salt = i === 0 ? "" : i === 1 ? "$" : `$${i - 1}`;
return safeIdentifier(identifier(desc) + "Valid" + salt);
}
/**
* Return an identifier for the given descriptor based on its type name.
*
* The type name for a protobuf message is the package name (if any), plus
* the names of parent messages it is nested in (if any), plus the name of
* the element, separated by dots. For example: foo.bar.ParentMsg.MyEnum.
*
* ECMAScript does not have packages or namespaces, so we need a single
* identifier. Our convention is to drop the package name, and to join other
* parts of the name with an underscore. For example: ParentMsg_MyEnum.
*/
function identifier(desc) {
const pkg = desc.file.proto.package;
const offset = pkg.length > 0 ? pkg.length + 1 : 0;
const nameWithoutPkg = desc.typeName.substring(offset);
return nameWithoutPkg.replace(/\./g, "_");
}
/**
* Compute all ideal names for the elements in the file, resolving name clashes.
*/
function allNames(file) {
const taken = new Set();
// In the first pass, register shape names
const shapeNames = new Map();
for (const desc of nestedTypes(file)) {
if (desc.kind != "enum" && desc.kind != "message") {
continue;
}
let name;
for (let i = 0;; i++) {
name = idealShapeName(desc, i);
if (!taken.has(name)) {
break;
}
}
taken.add(name);
shapeNames.set(desc, name);
}
// In the second pass, register desc names
const descNames = new Map();
for (const desc of [file, ...nestedTypes(file)]) {
let name;
switch (desc.kind) {
case "enum":
case "message": {
for (let i = 0;; i++) {
name = idealDescName(desc, i);
if (!taken.has(name)) {
break;
}
}
break;
}
default: {
for (let i = 0;; i++) {
name = idealDescName(desc, i);
if (!taken.has(name)) {
break;
}
}
break;
}
}
taken.add(name);
descNames.set(desc, name);
}
// In the third pass, register json type names
const jsonTypeNames = new Map();
for (const desc of nestedTypes(file)) {
if (desc.kind != "enum" && desc.kind != "message") {
continue;
}
let name;
for (let i = 0;; i++) {
name = idealJsonTypeName(desc, i);
if (!taken.has(name)) {
break;
}
}
taken.add(name);
jsonTypeNames.set(desc, name);
}
const validTypeNames = new Map();
for (const desc of nestedTypes(file)) {
if (desc.kind != "enum" && desc.kind != "message") {
continue;
}
let name;
for (let i = 0;; i++) {
name = idealValidTypeName(desc, i);
if (!taken.has(name)) {
break;
}
}
taken.add(name);
validTypeNames.set(desc, name);
}
return { shapeNames, jsonTypeNames, validTypeNames, descNames };
}