UNPKG

@bufbuild/protoplugin

Version:

Helps to create your own Protocol Buffers code generators.

415 lines (414 loc) 15 kB
// 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 { protoInt64, ScalarType, } from "@bufbuild/protobuf"; import { createImportSymbol } from "./import-symbol.js"; import { makeImportPathRelative } from "./import-path.js"; import { createJsDocTextFromDesc, formatJsDocBlock } from "./jsdoc.js"; export function createGeneratedFile(name, importPath, jsImportStyle, rewriteImport, resolveDescImport, resolveShapeImport, resolveJsonImport, resolveValidImport, createPreamble, runtime) { let preamble; const el = []; return { preamble(file) { preamble = createPreamble(file); }, print(printableOrFragments, ...rest) { let printables; if (printableOrFragments != null && Object.prototype.hasOwnProperty.call(printableOrFragments, "raw")) { // If called with a tagged template literal printables = buildPrintablesFromFragments(printableOrFragments, rest); } else { // If called with just an array of Printables printables = printableOrFragments != null ? [printableOrFragments, ...rest] : rest; } printableToEl({ el, runtime, resolveDescImport, resolveShapeImport, resolveJsonImport, resolveValidImport, }, printables); el.push("\n"); }, export(declaration, name) { return { kind: "es_export_stmt", name, declaration, }; }, string(string) { return { kind: "es_string", value: string, }; }, array(elements) { const p = []; p.push("["); for (const [index, element] of elements.entries()) { p.push(element); if (index < elements.length - 1) { p.push(", "); } } p.push("]"); return p; }, jsDoc(textOrSchema, indentation) { return { kind: "es_jsdoc", text: typeof textOrSchema == "string" ? textOrSchema : createJsDocTextFromDesc(textOrSchema), indentation, }; }, importSchema(schema, typeOnly = false) { return resolveDescImport(schema, typeOnly); }, importShape(schema) { return resolveShapeImport(schema); }, importJson(desc) { return resolveJsonImport(desc); }, importValid(desc) { return resolveValidImport(desc); }, import(name, from, typeOnly = false) { return createImportSymbol(name, from, typeOnly); }, jsImportStyle, runtime, getFileInfo() { return { name, content: elToContent(el, importPath, rewriteImport, jsImportStyle == "legacy_commonjs"), preamble, }; }, }; } function elToContent(el, importerPath, rewriteImportPath, legacyCommonJs) { if (el.length == 0) { return ""; } const c = []; if (legacyCommonJs) { c.push(`"use strict";\n`); c.push(`Object.defineProperty(exports, "__esModule", { value: true });\n`); c.push("\n"); } const symbolToIdentifier = processImports(el, importerPath, rewriteImportPath, (typeOnly, from, names) => { if (legacyCommonJs) { const p = names.map(({ name, alias }) => alias == undefined ? name : `${name}: ${alias}`); const what = `{ ${p.join(", ")} }`; c.push(`const ${what} = require(${escapeString(from)});\n`); } else { const p = names.map(({ name, alias }) => alias == undefined ? name : `${name} as ${alias}`); const what = `{ ${p.join(", ")} }`; if (typeOnly) { c.push(`import type ${what} from ${escapeString(from)};\n`); } else { c.push(`import ${what} from ${escapeString(from)};\n`); } } }); if (c.length > 0) { c.push("\n"); } const legacyCommonJsExportNames = []; for (const e of el) { if (typeof e == "string") { c.push(e); } else { switch (e.kind) { case "es_symbol": { const ident = symbolToIdentifier.get(e.id); if (ident != undefined) { c.push(ident); } break; } case "es_export_stmt": if (legacyCommonJs) { legacyCommonJsExportNames.push(e.name); } else { c.push("export "); } if (e.declaration !== undefined && e.declaration.length > 0) { c.push(e.declaration, " "); } c.push(e.name); break; } } } if (legacyCommonJs) { if (legacyCommonJsExportNames.length > 0) { c.push("\n"); } for (const name of legacyCommonJsExportNames) { c.push("exports.", name, " = ", name, ";\n"); } } return c.join(""); } function printableToEl(opt, printables) { const { el } = opt; for (const p of printables) { if (Array.isArray(p)) { printableToEl(opt, p); } else { switch (typeof p) { case "string": el.push(p); break; case "number": if (Number.isNaN(p)) { el.push("globalThis.NaN"); } else if (p === Number.POSITIVE_INFINITY) { el.push("globalThis.Infinity"); } else if (p === Number.NEGATIVE_INFINITY) { el.push("-globalThis.Infinity"); } else { el.push(p.toString(10)); } break; case "boolean": el.push(p.toString()); break; case "bigint": if (p == protoInt64.zero) { // Loose comparison will match between 0n and 0. el.push(opt.runtime.protoInt64, ".zero"); } else { el.push(opt.runtime.protoInt64, p > 0 ? ".uParse(" : ".parse(", escapeString(p.toString()), ")"); } break; case "object": if (p instanceof Uint8Array) { if (p.length === 0) { el.push("new Uint8Array(0)"); } else { el.push("new Uint8Array(["); const strings = []; for (const n of p) { strings.push("0x" + n.toString(16).toUpperCase().padStart(2, "0")); } el.push(strings.join(", ")); el.push("])"); } break; } switch (p.kind) { case "es_symbol": case "es_export_stmt": el.push(p); break; case "es_desc_ref": el.push(opt.resolveDescImport(p.desc, p.typeOnly)); break; case "es_shape_ref": el.push(opt.resolveShapeImport(p.desc)); break; case "es_json_type_ref": el.push(opt.resolveJsonImport(p.desc)); break; case "es_valid_type_ref": el.push(opt.resolveValidImport(p.desc)); break; case "es_jsdoc": el.push(formatJsDocBlock(p.text, p.indentation)); break; case "es_string": el.push(escapeString(p.value)); break; case "es_proto_int64": if (p.longAsString) { el.push(escapeString(p.value.toString())); } else { if (p.value == protoInt64.zero) { // Loose comparison will match between 0n and 0. el.push(opt.runtime.protoInt64, ".zero"); } else { switch (p.type) { case ScalarType.UINT64: case ScalarType.FIXED64: el.push(opt.runtime.protoInt64, ".uParse(", escapeString(p.value.toString()), ")"); break; default: el.push(opt.runtime.protoInt64, ".parse(", escapeString(p.value.toString()), ")"); break; } } } break; default: throw `cannot print ${typeof p}`; } break; default: throw `cannot print ${typeof p}`; } } } } function buildPrintablesFromFragments(fragments, values) { const printables = []; fragments.forEach((fragment, i) => { printables.push(fragment); if (fragments.length - 1 !== i) { printables.push(values[i]); } }); return printables; } function processImports(el, importerPath, rewriteImportPath, makeImportStatement) { // identifiers to use in the output const symbolToIdentifier = new Map(); // symbols that need a value import (as opposed to a type-only import) const symbolToIsValue = new Map(); // taken in this file const identifiersTaken = new Set(); // foreign symbols need an import const foreignSymbols = []; // Walk through all symbols used and populate the collections above. for (const s of el) { if (typeof s != "object") { continue; } switch (s.kind) { case "es_symbol": symbolToIdentifier.set(s.id, s.name); if (!s.typeOnly) { // a symbol is only type-imported as long as all uses are type-only symbolToIsValue.set(s.id, true); } if (s.from === importerPath) { identifiersTaken.add(s.name); } else { foreignSymbols.push(s); } break; case "es_export_stmt": identifiersTaken.add(s.name); break; } } // Walk through all foreign symbols and make their identifiers unique. const handledSymbols = new Set(); for (const s of foreignSymbols) { if (handledSymbols.has(s.id)) { continue; } handledSymbols.add(s.id); if (!identifiersTaken.has(s.name)) { identifiersTaken.add(s.name); continue; } let i = 1; let alias; for (;;) { // We choose '$' because it is invalid in proto identifiers. alias = `${s.name}$${i}`; if (!identifiersTaken.has(alias)) { break; } i++; } identifiersTaken.add(alias); symbolToIdentifier.set(s.id, alias); } const sourceToImport = new Map(); for (const s of foreignSymbols) { let i = sourceToImport.get(s.from); if (i == undefined) { i = { types: new Map(), values: new Map(), }; sourceToImport.set(s.from, i); } let alias = symbolToIdentifier.get(s.id); if (alias == s.name) { alias = undefined; } if (symbolToIsValue.get(s.id)) { i.values.set(s.name, alias); } else { i.types.set(s.name, alias); } } // Make import statements. const handledSource = new Set(); const buildNames = (map) => { const names = []; map.forEach((value, key) => names.push({ name: key, alias: value })); names.sort((a, b) => a.name.localeCompare(b.name)); return names; }; for (const s of foreignSymbols) { if (handledSource.has(s.from)) { continue; } handledSource.add(s.from); const i = sourceToImport.get(s.from); if (i == undefined) { // should never happen continue; } const from = makeImportPathRelative(importerPath, rewriteImportPath(s.from)); if (i.types.size > 0) { makeImportStatement(true, from, buildNames(i.types)); } if (i.values.size > 0) { makeImportStatement(false, from, buildNames(i.values)); } } return symbolToIdentifier; } function escapeString(value) { return ('"' + value .split("\\") .join("\\\\") .split('"') .join('\\"') .split("\r") .join("\\r") .split("\n") .join("\\n") + '"'); }