Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GeneratedFile.jsDoc() and other features to @bufbuild/protoplugin #649

Merged
merged 10 commits into from Dec 8, 2023
16 changes: 7 additions & 9 deletions packages/protoc-gen-es/src/declaration.ts
Expand Up @@ -25,9 +25,7 @@ import type {
} from "@bufbuild/protoplugin/ecmascript";
import {
getFieldTyping,
literalString,
localName,
makeJsDoc,
reifyWkt,
} from "@bufbuild/protoplugin/ecmascript";
import { getNonEditionRuntime } from "./editions.js";
Expand All @@ -48,13 +46,13 @@ export function generateDts(schema: Schema) {

// prettier-ignore
function generateEnum(schema: Schema, f: GeneratedFile, enumeration: DescEnum) {
f.print(makeJsDoc(enumeration));
f.print(f.jsDoc(enumeration));
f.print("export declare enum ", enumeration, " {");
for (const value of enumeration.values) {
if (enumeration.values.indexOf(value) > 0) {
f.print();
}
f.print(makeJsDoc(value, " "));
f.print(f.jsDoc(value, " "));
f.print(" ", localName(value), " = ", value.number, ",");
}
f.print("}");
Expand All @@ -73,7 +71,7 @@ function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage)
JsonReadOptions,
JsonValue
} = schema.runtime;
f.print(makeJsDoc(message));
f.print(f.jsDoc(message));
f.print("export declare class ", message, " extends ", Message, "<", message, "> {");
for (const member of message.members) {
switch (member.kind) {
Expand All @@ -90,7 +88,7 @@ function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage)
f.print();
generateWktMethods(schema, f, message);
f.print(" static readonly runtime: typeof ", protoN, ";");
f.print(' static readonly typeName = ', literalString(message.typeName), ';');
f.print(' static readonly typeName = ', f.string(message.typeName), ';');
f.print(" static readonly fields: ", FieldList, ";");
// In case we start supporting options, we have to surface them here
//f.print(" static readonly options: { readonly [extensionName: string]: ", rt.JsonValue, " } = {};")
Expand All @@ -116,13 +114,13 @@ function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage)

// prettier-ignore
function generateOneof(schema: Schema, f: GeneratedFile, oneof: DescOneof) {
f.print(makeJsDoc(oneof, " "));
f.print(f.jsDoc(oneof, " "));
f.print(" ", localName(oneof), ": {");
for (const field of oneof.fields) {
if (oneof.fields.indexOf(field) > 0) {
f.print(` } | {`);
}
f.print(makeJsDoc(field, " "));
f.print(f.jsDoc(field, " "));
const { typing } = getFieldTyping(field, f);
f.print(` value: `, typing, `;`);
f.print(` case: "`, localName(field), `";`);
Expand All @@ -131,7 +129,7 @@ function generateOneof(schema: Schema, f: GeneratedFile, oneof: DescOneof) {
}

function generateField(schema: Schema, f: GeneratedFile, field: DescField) {
f.print(makeJsDoc(field, " "));
f.print(f.jsDoc(field, " "));
const e: Printable = [];
e.push(" ", localName(field));
const { typing, optional } = getFieldTyping(field, f);
Expand Down
14 changes: 6 additions & 8 deletions packages/protoc-gen-es/src/javascript.ts
Expand Up @@ -27,9 +27,7 @@ import type {
} from "@bufbuild/protoplugin/ecmascript";
import {
getFieldExplicitDefaultValue,
literalString,
localName,
makeJsDoc,
reifyWkt,
} from "@bufbuild/protoplugin/ecmascript";
import { getNonEditionRuntime } from "./editions.js";
Expand All @@ -51,18 +49,18 @@ export function generateJs(schema: Schema) {
// prettier-ignore
function generateEnum(schema: Schema, f: GeneratedFile, enumeration: DescEnum) {
const protoN = getNonEditionRuntime(schema, enumeration.file);
f.print(makeJsDoc(enumeration));
f.print(f.jsDoc(enumeration));
f.print(f.exportDecl("const", enumeration), " = ", protoN, ".makeEnum(")
f.print(` "`, enumeration.typeName, `",`)
f.print(` [`)
if (enumeration.sharedPrefix === undefined) {
for (const value of enumeration.values) {
f.print(" {no: ", value.number, ", name: ", literalString(value.name), "},")
f.print(" {no: ", value.number, ", name: ", f.string(value.name), "},")
}
} else {
for (const value of enumeration.values) {
const localName = value.name.substring(enumeration.sharedPrefix.length);
f.print(" {no: ", value.number, ", name: ", literalString(value.name), ", localName: ", literalString(localName), "},")
f.print(" {no: ", value.number, ", name: ", f.string(value.name), ", localName: ", f.string(localName), "},")
}
}
f.print(` ],`)
Expand All @@ -73,9 +71,9 @@ function generateEnum(schema: Schema, f: GeneratedFile, enumeration: DescEnum) {
// prettier-ignore
function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage) {
const protoN = getNonEditionRuntime(schema, message.file);
f.print(makeJsDoc(message));
f.print(f.jsDoc(message));
f.print(f.exportDecl("const", message), " = ", protoN, ".makeMessageType(")
f.print(` `, literalString(message.typeName), `,`)
f.print(` `, f.string(message.typeName), `,`)
if (message.fields.length == 0) {
f.print(" [],")
} else {
Expand All @@ -90,7 +88,7 @@ function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage)
.makeMessageType(message.typeName, []).name;
if (needsLocalName) {
// local name is not inferrable from the type name, we need to provide it
f.print(` {localName: `, literalString(localName(message)), `},`)
f.print(` {localName: `, f.string(localName(message)), `},`)
}
f.print(");")
f.print()
Expand Down
16 changes: 7 additions & 9 deletions packages/protoc-gen-es/src/typescript.ts
Expand Up @@ -27,9 +27,7 @@ import type {
import {
getFieldIntrinsicDefaultValue,
getFieldTyping,
literalString,
localName,
makeJsDoc,
reifyWkt,
} from "@bufbuild/protoplugin/ecmascript";
import { generateFieldInfo } from "./javascript.js";
Expand All @@ -52,13 +50,13 @@ export function generateTs(schema: Schema) {
// prettier-ignore
function generateEnum(schema: Schema, f: GeneratedFile, enumeration: DescEnum) {
const protoN = getNonEditionRuntime(schema, enumeration.file);
f.print(makeJsDoc(enumeration));
f.print(f.jsDoc(enumeration));
f.print(f.exportDecl("enum", enumeration), " {");
for (const value of enumeration.values) {
if (enumeration.values.indexOf(value) > 0) {
f.print();
}
f.print(makeJsDoc(value, " "));
f.print(f.jsDoc(value, " "));
f.print(" ", localName(value), " = ", value.number, ",");
}
f.print("}");
Expand All @@ -83,7 +81,7 @@ function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage)
JsonReadOptions,
JsonValue
} = schema.runtime;
f.print(makeJsDoc(message));
f.print(f.jsDoc(message));
f.print(f.exportDecl("class", message), " extends ", Message, "<", message, "> {");
for (const member of message.members) {
switch (member.kind) {
Expand All @@ -103,7 +101,7 @@ function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage)
f.print();
generateWktMethods(schema, f, message);
f.print(" static readonly runtime: typeof ", protoN, " = ", protoN, ";");
f.print(' static readonly typeName = ', literalString(message.typeName), ';');
f.print(' static readonly typeName = ', f.string(message.typeName), ';');
f.print(" static readonly fields: ", FieldList, " = ", protoN, ".util.newFieldList(() => [");
for (const field of message.fields) {
generateFieldInfo(schema, f, field);
Expand Down Expand Up @@ -141,13 +139,13 @@ function generateMessage(schema: Schema, f: GeneratedFile, message: DescMessage)

// prettier-ignore
function generateOneof(schema: Schema, f: GeneratedFile, oneof: DescOneof) {
f.print(makeJsDoc(oneof, " "));
f.print(f.jsDoc(oneof, " "));
f.print(" ", localName(oneof), ": {");
for (const field of oneof.fields) {
if (oneof.fields.indexOf(field) > 0) {
f.print(` } | {`);
}
f.print(makeJsDoc(field, " "));
f.print(f.jsDoc(field, " "));
const { typing } = getFieldTyping(field, f);
f.print(` value: `, typing, `;`);
f.print(` case: "`, localName(field), `";`);
Expand All @@ -156,7 +154,7 @@ function generateOneof(schema: Schema, f: GeneratedFile, oneof: DescOneof) {
}

function generateField(schema: Schema, f: GeneratedFile, field: DescField) {
f.print(makeJsDoc(field, " "));
f.print(f.jsDoc(field, " "));
const e: Printable = [];
e.push(" ", localName(field));
const { defaultValue, typingInferrable } =
Expand Down
7 changes: 5 additions & 2 deletions packages/protoplugin-test/src/byo-transpile.test.ts
Expand Up @@ -89,7 +89,10 @@ describe("bring your own transpile", () => {
});
const res = plugin.run(req);
expect(res.file.length).toBeGreaterThanOrEqual(1);
const content = res.file[0]?.content ?? "";
return content.trim().split("\n");
let content = res.file[0]?.content ?? "";
if (content.endsWith("\n")) {
content = content.slice(0, -1); // trim final newline so we don't return an extra line
}
return content.split("\n");
}
});
138 changes: 138 additions & 0 deletions packages/protoplugin-test/src/deprecated-jsdoc.test.ts
@@ -0,0 +1,138 @@
// Copyright 2021-2023 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 { describe, expect, test } from "@jest/globals";
import { UpstreamProtobuf } from "upstream-protobuf";
import { CodeGeneratorRequest } from "@bufbuild/protobuf";
import type { Schema } from "@bufbuild/protoplugin/ecmascript";
import { createEcmaScriptPlugin } from "@bufbuild/protoplugin";
import type { GeneratedFile } from "@bufbuild/protoplugin/ecmascript";
import { createJsDocBlock, makeJsDoc } from "@bufbuild/protoplugin/ecmascript";

describe("deprecated makeJsDoc() and createJsDocBlock()", () => {
test("creates JSDoc comment block", async () => {
const lines = await testGenerate(`syntax="proto3";`, (f) => {
f.print(createJsDocBlock(`hello world`));
});
expect(lines).toStrictEqual(["/**", " * hello world", " */"]);
});

test("creates JSDoc comment block for message descriptor", async () => {
const lines = await testGenerate(
`
syntax="proto3";
message SomeMessage {};
`,
(f, schema) => {
f.print(makeJsDoc(schema.files[0].messages[0]));
},
);
expect(lines).toStrictEqual([
"/**",
" * @generated from message SomeMessage",
" */",
]);
});

test("creates JSDoc comment block for message descriptor with comments", async () => {
const lines = await testGenerate(
`
syntax="proto3";

// discarded detached comment

// comment on message
message SomeMessage {};
`,
(f, schema) => {
f.print(makeJsDoc(schema.files[0].messages[0]));
},
);
expect(lines).toStrictEqual([
"/**",
" * comment on message",
" *",
" * @generated from message SomeMessage",
" */",
]);
});

test("indents", async () => {
const lines = await testGenerate(`syntax="proto3";`, (f) => {
f.print(createJsDocBlock(`multi-line\ncomment`, " "));
});
expect(lines).toStrictEqual([
" /**",
" * multi-line",
" * comment",
" */",
]);
});

test("escapes */", async () => {
const lines = await testGenerate(`syntax="proto3";`, (f) => {
f.print(createJsDocBlock(`*/`));
});
expect(lines).toStrictEqual(["/**", " * *\\/", " */"]);
});

test("whitespace is unmodified", async () => {
const lines = await testGenerate(`syntax="proto3";`, (f) => {
f.print(createJsDocBlock(`\na\n b\n c\t`));
});
expect(lines).toStrictEqual([
"/**",
" *",
" * a",
" * b",
" * c\t",
" */",
]);
});

async function testGenerate(
protoContent: string,
gen: (f: GeneratedFile, schema: Schema) => void,
) {
const plugin = createEcmaScriptPlugin({
name: "test",
version: "v1",
generateTs: generateAny,
generateJs: generateAny,
generateDts: generateAny,
});

function generateAny(schema: Schema) {
gen(schema.generateFile("test.ts"), schema);
}

const upstream = new UpstreamProtobuf();
const protoFiles = {
"x.proto": protoContent,
};
const req = CodeGeneratorRequest.fromBinary(
await upstream.createCodeGeneratorRequest(protoFiles, {
parameter: "target=ts",
}),
);
expect(req.protoFile.length).toBe(1);
const res = plugin.run(req);
expect(res.file.length).toBeGreaterThanOrEqual(1);
let content = res.file[0]?.content ?? "";
if (content.endsWith("\n")) {
content = content.slice(0, -1); // trim final newline so we don't return an extra line
}
return content.split("\n");
}
});
7 changes: 5 additions & 2 deletions packages/protoplugin-test/src/file-export-decl.test.ts
Expand Up @@ -95,7 +95,10 @@ describe("file exportDecl", () => {
expect(req.protoFile[0]?.enumType.length).toBe(1);
const res = plugin.run(req);
expect(res.file.length).toBeGreaterThanOrEqual(1);
const content = res.file[0]?.content ?? "";
return content.trim().split("\n");
let content = res.file[0]?.content ?? "";
if (content.endsWith("\n")) {
content = content.slice(0, -1); // trim final newline so we don't return an extra line
}
return content.split("\n");
}
});