diff --git a/Makefile b/Makefile index b1ba843cd..a0367dab2 100644 --- a/Makefile +++ b/Makefile @@ -74,8 +74,9 @@ $(GEN)/protobuf-test: $(BUILD)/upstream-protobuf $(BUILD)/protoc-gen-es $(shell $(GEN)/protoplugin-test: $(BUILD)/protoc-gen-es $(shell find packages/protoplugin-test/proto -name '*.proto') @rm -rf packages/protoplugin-test/src/gen/* packages/protoplugin-test/descriptorset.bin - @npm run -w packages/protoplugin-test buf:build @npm run -w packages/protoplugin-test generate + @mkdir -p $(@D) + @touch $(@) $(GEN)/protobuf-conformance: $(BUILD)/upstream-protobuf $(BUILD)/protoc-gen-es npm run -w packages/protobuf-conformance generate diff --git a/packages/protoplugin-example/src/protoc-gen-twirp-es.ts b/packages/protoplugin-example/src/protoc-gen-twirp-es.ts index 983f6db2d..b23535b99 100755 --- a/packages/protoplugin-example/src/protoc-gen-twirp-es.ts +++ b/packages/protoplugin-example/src/protoc-gen-twirp-es.ts @@ -17,11 +17,7 @@ import { createEcmaScriptPlugin, runNodeJs } from "@bufbuild/protoplugin"; import { version } from "../package.json"; import type { Schema } from "@bufbuild/protoplugin/ecmascript"; -import { - literalString, - localName, - makeJsDoc, -} from "@bufbuild/protoplugin/ecmascript"; +import { localName } from "@bufbuild/protoplugin/ecmascript"; import { MethodKind } from "@bufbuild/protobuf"; const protocGenTwirpEs = createEcmaScriptPlugin({ @@ -40,7 +36,7 @@ function generateTs(schema: Schema) { JsonValue } = schema.runtime; for (const service of file.services) { - f.print(makeJsDoc(service)); + f.print(f.jsDoc(service)); f.print(f.exportDecl("class", localName(service) + "Client"), " {"); f.print(" private baseUrl: string = '';"); f.print(); @@ -75,11 +71,11 @@ function generateTs(schema: Schema) { for (const method of service.methods) { if (method.methodKind === MethodKind.Unary) { f.print(); - f.print(makeJsDoc(method, " ")); + f.print(f.jsDoc(method, " ")); f.print(" async ", localName(method), "(request: ", method.input, "): Promise<", method.output, "> {"); f.print(" const promise = this.request("); - f.print(" ", literalString(service.typeName), ","); - f.print(" ", literalString(method.name), ","); + f.print(" ", f.string(service.typeName), ","); + f.print(" ", f.string(method.name), ","); f.print(' "application/json",'); f.print(" request"); f.print(" );"); diff --git a/packages/protoplugin-test/package.json b/packages/protoplugin-test/package.json index 015a4bd73..db8f7c8ba 100644 --- a/packages/protoplugin-test/package.json +++ b/packages/protoplugin-test/package.json @@ -5,7 +5,6 @@ "clean": "rm -rf ./dist/cjs/* ./dist/esm/* ./dist/types/*", "build": "npm run build:esm+types", "build:esm+types": "../../node_modules/typescript/bin/tsc --project tsconfig.json --module ES2015 --verbatimModuleSyntax --outDir ./dist/esm --declaration --declarationDir ./dist/types", - "buf:build": "buf build -o descriptorset.bin", "generate": "buf generate", "test": "NODE_OPTIONS=--experimental-vm-modules npx jest" }, diff --git a/packages/protoplugin-test/proto/custom_options.proto b/packages/protoplugin-test/proto/custom_options.proto deleted file mode 100644 index 42bf7a043..000000000 --- a/packages/protoplugin-test/proto/custom_options.proto +++ /dev/null @@ -1,62 +0,0 @@ -// 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. - -syntax = "proto3"; - -import "google/protobuf/descriptor.proto"; - -package example; - -extend google.protobuf.FileOptions { - optional string example_file_option = 50000; -} -extend google.protobuf.MessageOptions { - optional int32 example_message_option = 50001; -} -extend google.protobuf.FieldOptions { - optional float example_field_option = 50002; -} -extend google.protobuf.OneofOptions { - optional int64 example_oneof_option = 50003; -} -extend google.protobuf.EnumOptions { - optional bool example_enum_option = 50004; -} -extend google.protobuf.EnumValueOptions { - optional uint32 example_enum_value_option = 50005; -} -extend google.protobuf.ServiceOptions { - optional ServiceStatus example_service_option = 50006; -} -extend google.protobuf.MethodOptions { - optional Configuration example_method_option = 50007; -} - -message Configuration { - int32 foo = 1; - string bar = 2; - oneof qux { - string quux = 3; - } - repeated string many = 4; - map mapping = 5; - string unused = 6; -} - -enum ServiceStatus { - UNDEFINED = 0; - EXPERIMENTAL = 1; - STABLE = 2; -} - diff --git a/packages/protoplugin-test/proto/address_book.proto b/packages/protoplugin-test/proto/option-enum.proto similarity index 81% rename from packages/protoplugin-test/proto/address_book.proto rename to packages/protoplugin-test/proto/option-enum.proto index edab10b71..87240cd69 100644 --- a/packages/protoplugin-test/proto/address_book.proto +++ b/packages/protoplugin-test/proto/option-enum.proto @@ -13,11 +13,11 @@ // limitations under the License. syntax = "proto3"; -package example; -import "proto/person.proto"; +package test; -// Our address book file is just one of these. -message AddressBook { - repeated Person people = 1; +// Used in custom-options.test.ts +enum OptionEnum { + OPTION_ENUM_UNSPECIFIED = 0; + OPTION_ENUM_A = 1; } diff --git a/packages/protoplugin-test/proto/person.proto b/packages/protoplugin-test/proto/option-message.proto similarity index 61% rename from packages/protoplugin-test/proto/person.proto rename to packages/protoplugin-test/proto/option-message.proto index bb8f7368f..602a578dd 100644 --- a/packages/protoplugin-test/proto/person.proto +++ b/packages/protoplugin-test/proto/option-message.proto @@ -13,28 +13,12 @@ // limitations under the License. syntax = "proto3"; -package example; -import "google/protobuf/timestamp.proto"; +package test; -message Person { - string name = 1; - int32 id = 2; // Unique ID number for this person. - string email = 3; - - enum PhoneType { - MOBILE = 0; - HOME = 1; - WORK = 2; - } - - message PhoneNumber { - string number = 1; - PhoneType type = 2; - } - - repeated PhoneNumber phones = 4; - - google.protobuf.Timestamp last_updated = 5; +// Used in custom-options.test.ts +message OptionMessage { + int32 foo = 1; + string bar = 2; + repeated string many = 4; } - diff --git a/packages/protoplugin-test/proto/service.proto b/packages/protoplugin-test/proto/service.proto deleted file mode 100644 index e2d2fb565..000000000 --- a/packages/protoplugin-test/proto/service.proto +++ /dev/null @@ -1,63 +0,0 @@ -// 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. - -syntax = "proto3"; - -import "proto/custom_options.proto"; - -package example; - -option (example_file_option) = "Hello"; - -message MessageWithOptions { - option (example_message_option) = 1234; - - int32 foo = 1 [(example_field_option) = 1.23]; - string bar = 2; - oneof qux { - option (example_oneof_option) = 42; - - string quux = 3; - } - repeated string many = 4; - map mapping = 5; - string unused = 6; -} - -enum EnumWithOptions { - option (example_enum_option) = true; - - UNSET = 0; - ACTIVE = 1 [(example_enum_value_option) = 321]; - INACTIVE = 2; -} - -message GetRequest {} -message GetResponse {} - -service ServiceWithOptions { - option (example_service_option) = EXPERIMENTAL; - - rpc Get(GetRequest) returns (GetResponse) { - option deprecated = true; - option (example_method_option) = { - foo: 567, - bar: "Some string", - quux: "Oneof string", - many: ["a", "b", "c"], - mapping: [{key: "testKey", value: "testVal"}] - }; - } -} - diff --git a/packages/protoplugin-test/src/byo-transpile.test.ts b/packages/protoplugin-test/src/byo-transpile.test.ts index 060d3e8a7..baa6ac92f 100644 --- a/packages/protoplugin-test/src/byo-transpile.test.ts +++ b/packages/protoplugin-test/src/byo-transpile.test.ts @@ -13,30 +13,30 @@ // limitations under the License. import { describe, expect, test } from "@jest/globals"; -import { CodeGeneratorRequest } from "@bufbuild/protobuf"; -import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; import type { FileInfo } from "@bufbuild/protoplugin/ecmascript"; +import { createTestPluginAndRun } from "./helpers"; describe("bring your own transpile", () => { - test("does not transpile target=ts", () => { - const lines = testGenerate("target=ts"); + test("does not transpile target=ts", async () => { + const lines = await testGenerate("target=ts"); expect(lines).toStrictEqual(["fake typescript source"]); }); - test("transpiles to target js", () => { - const lines = testGenerate("target=js"); + test("transpiles to target js", async () => { + const lines = await testGenerate("target=js"); expect(lines).toStrictEqual(["fake transpiled to js"]); }); - test("transpiles to target dts", () => { - const lines = testGenerate("target=dts"); + test("transpiles to target dts", async () => { + const lines = await testGenerate("target=dts"); expect(lines).toStrictEqual(["fake transpiled to dts"]); }); - function testGenerate(parameter: string): string[] { - const plugin = createEcmaScriptPlugin({ - name: "test", - version: "v1", + async function testGenerate(parameter: string) { + return await createTestPluginAndRun({ + returnLinesOfFirstFile: true, + proto: `syntax="proto3";`, + parameter, generateTs: (schema) => { const f = schema.generateFile("test.ts"); f.print("fake typescript source"); @@ -84,15 +84,5 @@ describe("bring your own transpile", () => { return out; }, }); - const req = new CodeGeneratorRequest({ - parameter, - }); - 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"); } }); diff --git a/packages/protoplugin-test/src/create-es-plugin.test.ts b/packages/protoplugin-test/src/create-es-plugin.test.ts deleted file mode 100644 index e988ba271..000000000 --- a/packages/protoplugin-test/src/create-es-plugin.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -// 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 { beforeEach, describe, expect, test } from "@jest/globals"; -import { getCodeGeneratorRequest } from "./helpers.js"; -import type { CodeGeneratorRequest } from "@bufbuild/protobuf"; -import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; -import type { Plugin } from "@bufbuild/protoplugin"; -import type { Schema, Target } from "@bufbuild/protoplugin/ecmascript"; -import { makeJsDoc } from "@bufbuild/protoplugin/ecmascript"; - -type OutFixture = { - [key in Target as string]: string[]; -}; - -// prettier-ignore -function generateFile(schema: Schema, extension: string) { - for (const file of schema.files) { - const f = schema.generateFile(file.name + extension); - f.preamble(file); - for (const enumeration of file.enums) { - f.print(makeJsDoc(enumeration)); - } - const { Message } = schema.runtime; - const MessageAsType = Message.toTypeOnly(); - f.print("interface Todo {"); - f.print(" title: string;"); - f.print(" desc: string;"); - f.print("}"); - f.print(); - f.print("export class Test {"); - f.print(); - f.print(" print>(data: T, todo: Partial): Promise {"); - f.print(" const headers = new Headers([]);"); - f.print(" console.log(headers);"); - f.print(" console.log(data);"); - f.print(" console.log(todo);"); - f.print(" return new Promise((resolve, reject) => {"); - f.print(" resolve('test');"); - f.print(" });"); - f.print(" }"); - f.print("}"); - } -} - -function generateTs(schema: Schema) { - generateFile(schema, "_proto.ts"); -} - -function generateJs(schema: Schema) { - generateFile(schema, "_proto.js"); -} - -function generateDts(schema: Schema) { - generateFile(schema, "_proto.dts"); -} - -function verifyOutFiles( - plugin: Plugin, - fixture: OutFixture, - req?: CodeGeneratorRequest, -) { - const targets = Object.keys(fixture); - req = - req ?? - getCodeGeneratorRequest(`target=${targets.join("+")}`, [ - "proto/address_book.proto", - "proto/person.proto", - ]); - const resp = plugin.run(req); - - // The total expected files is the sum of the lengths of the arrays in the - // given fixture. - const totalExpectedFiles = Object.values(fixture).reduce( - (prev, curr) => prev + curr.length, - 0, - ); - expect(resp.file.length).toEqual(totalExpectedFiles); - - targets.forEach((target) => { - const expectedFiles = fixture[target]; - expectedFiles.forEach((file) => { - expect(resp.file.findIndex((e) => e.name === file)).toBeGreaterThan(-1); - }); - }); -} - -/** - * The create-es-plugin tests verify the number and name of files output via the - * plugin process, using various target outs and various provided generator functions - */ -describe("all generators with variant target outs", function () { - let protocGenEs: Plugin; - beforeEach(() => { - protocGenEs = createEcmaScriptPlugin({ - name: "test-plugin", - version: "v0.1.0", - generateTs, - generateJs, - generateDts, - }); - }); - test("all targets", () => { - verifyOutFiles(protocGenEs, { - ts: ["proto/person_proto.ts", "proto/address_book_proto.ts"], - js: ["proto/person_proto.js", "proto/address_book_proto.js"], - dts: ["proto/person_proto.dts", "proto/address_book_proto.dts"], - }); - }); - test("ts+js", () => { - verifyOutFiles(protocGenEs, { - ts: ["proto/person_proto.ts", "proto/address_book_proto.ts"], - js: ["proto/person_proto.js", "proto/address_book_proto.js"], - }); - }); - test("ts+dts", () => { - verifyOutFiles(protocGenEs, { - ts: ["proto/person_proto.ts", "proto/address_book_proto.ts"], - dts: ["proto/person_proto.dts", "proto/address_book_proto.dts"], - }); - }); - test("ts", () => { - verifyOutFiles(protocGenEs, { - ts: ["proto/person_proto.ts", "proto/address_book_proto.ts"], - }); - }); - test("js", () => { - // Note the TS generator was not run because we only specified js+dts - // and provided a generator for both, so there was no need for TS files - verifyOutFiles(protocGenEs, { - js: ["proto/person_proto.js", "proto/address_book_proto.js"], - }); - }); - test("dts", () => { - // Note the TS generator was not run because we only specified js+dts - // and provided a generator for both, so there was no need for TS files - verifyOutFiles(protocGenEs, { - dts: ["proto/person_proto.dts", "proto/address_book_proto.dts"], - }); - }); - test("js+dts", () => { - // Note the TS generator was not run because we only specified js+dts - // and provided a generator for both, so there was no need for TS files - verifyOutFiles(protocGenEs, { - js: ["proto/person_proto.js", "proto/address_book_proto.js"], - dts: ["proto/person_proto.dts", "proto/address_book_proto.dts"], - }); - }); -}); - -describe("no declaration generator with variant target outs", function () { - let protocGenEs: Plugin; - beforeEach(() => { - protocGenEs = createEcmaScriptPlugin({ - name: "test-plugin", - version: "v0.1.0", - generateTs, - generateJs, - }); - }); - // In all the tests below, we verify that a declaration file was transpiled - // based on it having an extension of "d.ts", which is what the transpiler - // generates. Our custom generateDts above uses 'dts'. A better approach - // would be to use a spy and verify which functions are being called, but - // Jest currently has an issue with importing the Jest object in TypeScript - test("all targets", () => { - verifyOutFiles(protocGenEs, { - ts: ["proto/person_proto.ts", "proto/address_book_proto.ts"], - js: ["proto/person_proto.js", "proto/address_book_proto.js"], - dts: ["proto/person_proto.d.ts", "proto/address_book_proto.d.ts"], - }); - }); - test("ts+js", () => { - verifyOutFiles(protocGenEs, { - ts: ["proto/person_proto.ts", "proto/address_book_proto.ts"], - js: ["proto/person_proto.js", "proto/address_book_proto.js"], - }); - }); - test("ts+dts", () => { - verifyOutFiles(protocGenEs, { - ts: ["proto/person_proto.ts", "proto/address_book_proto.ts"], - dts: ["proto/person_proto.d.ts", "proto/address_book_proto.d.ts"], - }); - }); - test("ts", () => { - verifyOutFiles(protocGenEs, { - ts: ["proto/person_proto.ts", "proto/address_book_proto.ts"], - }); - }); - test("js", () => { - verifyOutFiles(protocGenEs, { - js: ["proto/person_proto.js", "proto/address_book_proto.js"], - }); - }); - test("dts", () => { - verifyOutFiles(protocGenEs, { - dts: ["proto/person_proto.d.ts", "proto/address_book_proto.d.ts"], - }); - }); - test("js+dts", () => { - // Note that even though we only requested js+dts, the TS generator - // ran also because we need it to emit the declaration files. However, - // there should be no TS files in the generated output since ts was - // not specified as a target out. - verifyOutFiles(protocGenEs, { - js: ["proto/person_proto.js", "proto/address_book_proto.js"], - dts: ["proto/person_proto.d.ts", "proto/address_book_proto.d.ts"], - }); - }); -}); - -describe("only request one file to generate with variant target outs", function () { - test("all targets with all generators", () => { - const req = getCodeGeneratorRequest("target=ts+js+dts", [ - "proto/address_book.proto", - ]); - const protocGenEs = createEcmaScriptPlugin({ - name: "test-plugin", - version: "v0.1.0", - generateTs, - generateJs, - generateDts, - }); - verifyOutFiles( - protocGenEs, - { - ts: ["proto/address_book_proto.ts"], - js: ["proto/address_book_proto.js"], - dts: ["proto/address_book_proto.dts"], - }, - req, - ); - }); - test("all targets with no dts generator", () => { - const req = getCodeGeneratorRequest("target=ts+js+dts", [ - "proto/address_book.proto", - ]); - const protocGenEs = createEcmaScriptPlugin({ - name: "test-plugin", - version: "v0.1.0", - generateTs, - generateJs, - }); - verifyOutFiles( - protocGenEs, - { - ts: ["proto/address_book_proto.ts"], - js: ["proto/address_book_proto.js"], - dts: ["proto/address_book_proto.d.ts"], - }, - req, - ); - }); - test("all targets with no js or dts generator", () => { - const req = getCodeGeneratorRequest("target=ts+js+dts", [ - "proto/address_book.proto", - ]); - const protocGenEs = createEcmaScriptPlugin({ - name: "test-plugin", - version: "v0.1.0", - generateTs, - }); - verifyOutFiles( - protocGenEs, - { - ts: ["proto/address_book_proto.ts"], - js: ["proto/address_book_proto.js"], - dts: ["proto/address_book_proto.d.ts"], - }, - req, - ); - }); -}); diff --git a/packages/protoplugin-test/src/custom-options.test.ts b/packages/protoplugin-test/src/custom-options.test.ts index 183678f31..0a4c6d685 100644 --- a/packages/protoplugin-test/src/custom-options.test.ts +++ b/packages/protoplugin-test/src/custom-options.test.ts @@ -13,232 +13,150 @@ // limitations under the License. import { describe, expect, test } from "@jest/globals"; -import { assert, getDescriptorSet } from "./helpers.js"; import { - findCustomScalarOption, - findCustomMessageOption, findCustomEnumOption, + findCustomMessageOption, + findCustomScalarOption, } from "@bufbuild/protoplugin/ecmascript"; -import { proto3, ScalarType } from "@bufbuild/protobuf"; -import { Configuration, ServiceStatus } from "./gen/proto/custom_options_pb.js"; - -describe("custom options", function () { - const enumName = "example.EnumWithOptions"; - const msgName = "example.MessageWithOptions"; - const serviceName = "example.ServiceWithOptions"; - const fileName = "proto/service"; - - const descriptorSet = getDescriptorSet(); +import type { DescFile } from "@bufbuild/protobuf"; +import { createDescriptorSet, ScalarType } from "@bufbuild/protobuf"; +import { UpstreamProtobuf } from "upstream-protobuf"; +import { readFileSync } from "node:fs"; +import { OptionEnum } from "./gen/proto/option-enum_pb.js"; +import { OptionMessage } from "./gen/proto/option-message_pb.js"; - describe("finds options correctly", function () { - test("file options", () => { - for (const file of descriptorSet.files) { - if (file.name === fileName) { - expect( - findCustomScalarOption(file, 50000, ScalarType.STRING), - ).toEqual("Hello"); +describe("custom options", () => { + describe("findCustomScalarOption on file descriptor", () => { + test("finds DOUBLE", async () => { + const descFile = await compileToSet(` + syntax="proto3"; + import "google/protobuf/descriptor.proto"; + package test; + extend google.protobuf.FileOptions { + optional double option = 60123; } - } + option (test.option) = 3.142; + `); + const value = findCustomScalarOption(descFile, 60123, ScalarType.DOUBLE); + expect(value).toBe(3.142); }); - test("enum options", () => { - const enumeration = descriptorSet.enums.get(enumName); - assert(enumeration); - expect( - findCustomScalarOption(enumeration, 50004, ScalarType.BOOL), - ).toBeTruthy(); - }); - test("enum value options", () => { - const enumeration = descriptorSet.enums.get(enumName); - assert(enumeration); - for (const enumValue of enumeration.values) { - if (enumValue.name === "ACTIVE") { - expect( - findCustomScalarOption(enumValue, 50005, ScalarType.UINT32), - ).toEqual(321); - } else { - expect( - findCustomScalarOption(enumValue, 50005, ScalarType.UINT32), - ).toBeUndefined(); + test("finds UINT64", async () => { + const descFile = await compileToSet(` + syntax="proto3"; + import "google/protobuf/descriptor.proto"; + package test; + extend google.protobuf.FileOptions { + optional uint64 option = 60123; } - } - }); - test("message options", () => { - const msg = descriptorSet.messages.get(msgName); - assert(msg); - expect(findCustomScalarOption(msg, 50001, ScalarType.INT32)).toEqual( - 1234, - ); + option (test.option) = 123456789; + `); + const value = findCustomScalarOption(descFile, 60123, ScalarType.UINT64); + expect(value).toBe(123456789n); }); - test("field options", () => { - const msg = descriptorSet.messages.get(msgName); - assert(msg); - for (const member of msg.members) { - switch (member.kind) { - case "oneof": - expect( - findCustomScalarOption(member, 50003, ScalarType.INT64), - ).toEqual(BigInt(42)); - break; - default: { - const val = findCustomScalarOption(member, 50002, ScalarType.FLOAT); - if (member.name === "foo") { - // The custom option value is set to 1.23, but due to the representation - // of floating-point numbers in JavaScript, we need to compare the parsed - // result to the nearest 32-bit single precision value. - expect(val).toEqual(Math.fround(1.23)); - } else { - expect(val).toBeUndefined(); - } - break; - } + test("finds STRING", async () => { + const descFile = await compileToSet(` + syntax="proto3"; + import "google/protobuf/descriptor.proto"; + package test; + extend google.protobuf.FileOptions { + optional string option = 60123; } - } - }); - test("service options", () => { - const service = descriptorSet.services.get(serviceName); - assert(service); - const enumVal: ServiceStatus | undefined = findCustomEnumOption( - service, - 50006, - ); - expect(enumVal).toEqual(ServiceStatus.EXPERIMENTAL); + option (test.option) = "foo"; + `); + const value = findCustomScalarOption(descFile, 60123, ScalarType.STRING); + expect(value).toBe("foo"); }); - test("method options", () => { - const service = descriptorSet.services.get(serviceName); - assert(service); - for (const method of service.methods) { - const option = findCustomMessageOption(method, 50007, Configuration); - expect(option?.foo).toEqual(567); - expect(option?.bar).toEqual("Some string"); - expect(option?.qux.case).toEqual("quux"); - expect(option?.qux.value).toEqual("Oneof string"); - expect(option?.many).toEqual(["a", "b", "c"]); - expect(option?.mapping).toEqual({ testKey: "testVal" }); - } + test("finds BOOL", async () => { + const descFile = await compileToSet(` + syntax="proto3"; + import "google/protobuf/descriptor.proto"; + package test; + extend google.protobuf.FileOptions { + optional bool option = 60123; + } + option (test.option) = true; + `); + const value = findCustomScalarOption(descFile, 60123, ScalarType.BOOL); + expect(value).toBe(true); }); }); - describe("all methods return undefined when option not found", function () { - test("file options", () => { - for (const file of descriptorSet.files) { - if (file.name === fileName) { - expect( - findCustomScalarOption(file, 99999, ScalarType.STRING), - ).toBeUndefined(); + describe("findCustomEnumOption on file descriptor", () => { + test("finds enum", async () => { + const descFile = await compileToSet(` + syntax="proto3"; + import "google/protobuf/descriptor.proto"; + import "option-enum.proto"; + package test; + extend google.protobuf.FileOptions { + optional test.OptionEnum option = 60123; } - } - }); - test("enum options", () => { - const enumeration = descriptorSet.enums.get(enumName); - assert(enumeration); - expect( - findCustomScalarOption(enumeration, 99999, ScalarType.BOOL), - ).toBeUndefined(); - }); - test("enum value options", () => { - const enumeration = descriptorSet.enums.get(enumName); - assert(enumeration); - for (const enumValue of enumeration.values) { - expect( - findCustomScalarOption(enumValue, 99999, ScalarType.UINT32), - ).toBeUndefined(); - } + option (test.option) = OPTION_ENUM_A; + `); + const value = findCustomEnumOption(descFile, 60123); + expect(value).toBe(OptionEnum.A); }); - test("message options", () => { - const msg = descriptorSet.messages.get(msgName); - assert(msg); - expect( - findCustomScalarOption(msg, 99999, ScalarType.INT32), - ).toBeUndefined(); - }); - test("field options", () => { - const msg = descriptorSet.messages.get(msgName); - - const quxField = msg?.members.find((m) => m.name === "qux"); - assert(quxField); - assert(quxField.kind === "oneof"); - expect( - findCustomScalarOption(quxField, 99999, ScalarType.INT64), - ).toBeUndefined(); - - const fooField = msg?.members.find((m) => m.name === "foo"); - assert(fooField); - assert(fooField.kind === "field"); - expect( - findCustomScalarOption(fooField, 99999, ScalarType.FLOAT), - ).toBeUndefined(); - }); - test("service options", () => { - const service = descriptorSet.services.get(serviceName); - assert(service); - expect(findCustomEnumOption(service, 99999)).toBeUndefined(); - }); - test("method options", () => { - const service = descriptorSet.services.get(serviceName); - assert(service); - for (const method of service.methods) { - expect( - findCustomMessageOption(method, 99999, Configuration), - ).toBeUndefined(); - } + test("returns undefined if not set", async () => { + const descFile = await compileToSet(` + syntax="proto3"; + package test; + `); + const value = findCustomEnumOption(descFile, 60123); + expect(value).toBeUndefined(); }); }); - describe("custom options with message type", function () { - test("invalid message throws", () => { - const invalid = proto3.makeMessageType("InvalidMessage", [ - { - no: 1, - name: "invalid", - kind: "scalar", - T: ScalarType.FLOAT, - }, - ]); - const service = descriptorSet.services.get(serviceName); - assert(service); - for (const method of service.methods) { - const getFn = () => { - findCustomMessageOption(method, 50007, invalid); + describe("findCustomMessageOption on file descriptor", () => { + test("finds message", async () => { + const descFile = await compileToSet(` + syntax="proto3"; + import "google/protobuf/descriptor.proto"; + import "option-message.proto"; + package test; + extend google.protobuf.FileOptions { + optional test.OptionMessage option = 60123; + } + option (test.option) = { + foo: 567, + bar: "Some string", + many: ["a", "b", "c"], }; - expect(getFn).toThrow(Error); - } + `); + const value = findCustomMessageOption(descFile, 60123, OptionMessage); + expect(value).toBeDefined(); + expect(value?.foo).toBe(567); + expect(value?.bar).toBe("Some string"); + expect(value?.many).toStrictEqual(["a", "b", "c"]); + }); + test("returns undefined if not set", async () => { + const descFile = await compileToSet(` + syntax="proto3"; + package test; + `); + const value = findCustomMessageOption(descFile, 60123, OptionMessage); + expect(value).toBeUndefined(); }); }); - test("valid but partial message still returns values", () => { - // Rather than use the generated Configuration message, we are using a type created at - // runtime to test 1. that makeMessageType also allows us to get message options - // and 2. that a partial message will work. - const partial = proto3.makeMessageType("PartialMessage", [ + + async function compileToSet(proto: string): Promise { + const upstream = new UpstreamProtobuf(); + const setBin = await upstream.compileToDescriptorSet( { - no: 1, - name: "foo", - kind: "scalar", - T: ScalarType.INT32, + "input.proto": proto, + "option-enum.proto": readFileSync("proto/option-enum.proto", "utf-8"), + "option-message.proto": readFileSync( + "proto/option-message.proto", + "utf-8", + ), }, { - no: 2, - name: "bar", - kind: "scalar", - T: ScalarType.STRING, + includeImports: true, + retainOptions: true, }, - ]); - const service = descriptorSet.services.get(serviceName); - assert(service); - for (const method of service.methods) { - const option = findCustomMessageOption(method, 50007, partial); - expect(option?.foo).toEqual(567); - expect(option?.bar).toEqual("Some string"); - // Following values were set in the proto file but not in the message type above - expect(option?.quux).toBeUndefined(); - expect(option?.many).toBeUndefined(); - expect(option?.mapping).toBeUndefined(); - expect(option?.unused).toBeUndefined(); + ); + const set = createDescriptorSet(setBin); + const file = set.files.find((f) => f.proto.name === "input.proto"); + if (file === undefined) { + throw new Error("missing file descriptor"); } - }); - test("unset properties in proto return default values", () => { - const service = descriptorSet.services.get(serviceName); - assert(service); - for (const method of service.methods) { - const option = findCustomMessageOption(method, 50007, Configuration); - expect(option?.unused).toEqual(""); - } - }); + return file; + } }); diff --git a/packages/protoplugin-test/src/deprecated-jsdoc.test.ts b/packages/protoplugin-test/src/deprecated-jsdoc.test.ts index bd5f1264c..64a798efd 100644 --- a/packages/protoplugin-test/src/deprecated-jsdoc.test.ts +++ b/packages/protoplugin-test/src/deprecated-jsdoc.test.ts @@ -13,31 +13,32 @@ // 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"; +import { createTestPluginAndRun } from "./helpers"; describe("deprecated makeJsDoc() and createJsDocBlock()", () => { test("creates JSDoc comment block", async () => { - const lines = await testGenerate(`syntax="proto3";`, (f) => { - f.print(createJsDocBlock(`hello world`)); + const lines = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + generateAny(f) { + f.print(createJsDocBlock(`hello world`)); + }, + returnLinesOfFirstFile: true, }); expect(lines).toStrictEqual(["/**", " * hello world", " */"]); }); test("creates JSDoc comment block for message descriptor", async () => { - const lines = await testGenerate( - ` + const lines = await createTestPluginAndRun({ + proto: ` syntax="proto3"; message SomeMessage {}; - `, - (f, schema) => { + `, + generateAny(f, schema) { f.print(makeJsDoc(schema.files[0].messages[0])); }, - ); + returnLinesOfFirstFile: true, + }); expect(lines).toStrictEqual([ "/**", " * @generated from message SomeMessage", @@ -46,19 +47,20 @@ describe("deprecated makeJsDoc() and createJsDocBlock()", () => { }); test("creates JSDoc comment block for message descriptor with comments", async () => { - const lines = await testGenerate( - ` + const lines = await createTestPluginAndRun({ + proto: ` syntax="proto3"; // discarded detached comment // comment on message message SomeMessage {}; - `, - (f, schema) => { + `, + generateAny(f, schema) { f.print(makeJsDoc(schema.files[0].messages[0])); }, - ); + returnLinesOfFirstFile: true, + }); expect(lines).toStrictEqual([ "/**", " * comment on message", @@ -69,8 +71,12 @@ describe("deprecated makeJsDoc() and createJsDocBlock()", () => { }); test("indents", async () => { - const lines = await testGenerate(`syntax="proto3";`, (f) => { - f.print(createJsDocBlock(`multi-line\ncomment`, " ")); + const lines = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + generateAny(f) { + f.print(createJsDocBlock(`multi-line\ncomment`, " ")); + }, + returnLinesOfFirstFile: true, }); expect(lines).toStrictEqual([ " /**", @@ -81,15 +87,23 @@ describe("deprecated makeJsDoc() and createJsDocBlock()", () => { }); test("escapes */", async () => { - const lines = await testGenerate(`syntax="proto3";`, (f) => { - f.print(createJsDocBlock(`*/`)); + const lines = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + generateAny(f) { + f.print(createJsDocBlock(`*/`)); + }, + returnLinesOfFirstFile: true, }); expect(lines).toStrictEqual(["/**", " * *\\/", " */"]); }); test("whitespace is unmodified", async () => { - const lines = await testGenerate(`syntax="proto3";`, (f) => { - f.print(createJsDocBlock(`\na\n b\n c\t`)); + const lines = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + generateAny(f) { + f.print(createJsDocBlock(`\na\n b\n c\t`)); + }, + returnLinesOfFirstFile: true, }); expect(lines).toStrictEqual([ "/**", @@ -100,39 +114,4 @@ describe("deprecated makeJsDoc() and createJsDocBlock()", () => { " */", ]); }); - - 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"); - } }); diff --git a/packages/protoplugin-test/src/file-export-decl.test.ts b/packages/protoplugin-test/src/file-export-decl.test.ts index 4acb2835f..3aa43db5f 100644 --- a/packages/protoplugin-test/src/file-export-decl.test.ts +++ b/packages/protoplugin-test/src/file-export-decl.test.ts @@ -13,12 +13,9 @@ // limitations under the License. import { describe, expect, test } from "@jest/globals"; -import { UpstreamProtobuf } from "upstream-protobuf"; import type { DescEnum, DescMessage } from "@bufbuild/protobuf"; -import { CodeGeneratorRequest } from "@bufbuild/protobuf"; -import type { Schema } from "@bufbuild/protoplugin"; -import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; import type { GeneratedFile } from "@bufbuild/protoplugin/ecmascript"; +import { createTestPluginAndRun } from "./helpers"; describe("file exportDecl", () => { test("works as documented", async () => { @@ -58,47 +55,19 @@ describe("file exportDecl", () => { descEnum: DescEnum, ) => void, ) { - const plugin = createEcmaScriptPlugin({ - name: "test", - version: "v1", - generateTs: generateAny, - generateJs: generateAny, - generateDts: generateAny, - }); - - function generateAny(schema: Schema) { - gen( - schema.generateFile("test.ts"), - schema.files[0].messages[0], - schema.files[0].enums[0], - ); - } - - const upstream = new UpstreamProtobuf(); - const protoFiles = { - "x.proto": ` + return await createTestPluginAndRun({ + proto: ` syntax="proto3"; message SomeMessage {} enum SomeEnum { SOME_ENUM_UNRECOGNIZED = 0; SOME_ENUM_A = 1; - } - `, - }; - const req = CodeGeneratorRequest.fromBinary( - await upstream.createCodeGeneratorRequest(protoFiles, { - parameter: "target=ts", - }), - ); - expect(req.protoFile.length).toBe(1); - expect(req.protoFile[0]?.messageType.length).toBe(1); - expect(req.protoFile[0]?.enumType.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"); + }`, + parameter: "target=ts", + generateAny(f, schema) { + gen(f, schema.files[0].messages[0], schema.files[0].enums[0]); + }, + returnLinesOfFirstFile: true, + }); } }); diff --git a/packages/protoplugin-test/src/file-import.test.ts b/packages/protoplugin-test/src/file-import.test.ts new file mode 100644 index 000000000..05386fffd --- /dev/null +++ b/packages/protoplugin-test/src/file-import.test.ts @@ -0,0 +1,88 @@ +// 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 { createTestPluginAndRun } from "./helpers"; + +describe("file import", () => { + test("should create import symbol for package", async function () { + await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=ts", + generateAny(f) { + const imp = f.import("Foo", "@scope/pkg"); + expect(imp.name).toBe("Foo"); + expect(imp.from).toBe("@scope/pkg"); + expect(imp.typeOnly).toBe(false); + }, + }); + }); + test("should create import symbol for relative import", async function () { + await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=ts", + generateAny(f) { + const imp = f.import("Foo", "./foo_zz.js"); + expect(imp.name).toBe("Foo"); + expect(imp.from).toBe("./foo_zz.js"); + expect(imp.typeOnly).toBe(false); + }, + }); + }); + test("should create import symbol for https import", async function () { + await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=ts", + generateAny(f) { + const imp = f.import("Foo", "https://example.com/foo.js"); + expect(imp.name).toBe("Foo"); + expect(imp.from).toBe("https://example.com/foo.js"); + expect(imp.typeOnly).toBe(false); + }, + }); + }); + test("should create import symbol for enum descriptor", async function () { + await createTestPluginAndRun({ + proto: ` + syntax="proto3"; + enum Foo { + FOO_UNSPECIFIED = 0; + FOO_BAR = 1; + } + `, + parameter: "target=ts", + generateAny(f, schema) { + const imp = f.import(schema.files[0].enums[0]); + expect(imp.name).toBe("Foo"); + expect(imp.from).toBe("./x_pb.js"); + expect(imp.typeOnly).toBe(false); + }, + }); + }); + test("should create import symbol for message descriptor", async function () { + await createTestPluginAndRun({ + proto: ` + syntax="proto3"; + message Person {} + `, + parameter: "target=ts", + generateAny(f, schema) { + const imp = f.import(schema.files[0].messages[0]); + expect(imp.name).toBe("Person"); + expect(imp.from).toBe("./x_pb.js"); + expect(imp.typeOnly).toBe(false); + }, + }); + }); +}); diff --git a/packages/protoplugin-test/src/file-jsdoc.test.ts b/packages/protoplugin-test/src/file-jsdoc.test.ts index d1193692e..a7edf9ce2 100644 --- a/packages/protoplugin-test/src/file-jsdoc.test.ts +++ b/packages/protoplugin-test/src/file-jsdoc.test.ts @@ -13,30 +13,34 @@ // 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"; -import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; -import type { GeneratedFile } from "@bufbuild/protoplugin/ecmascript"; +import { createTestPluginAndRun } from "./helpers"; describe("file jsDoc", () => { test("creates JSDoc comment block", async () => { - const lines = await testGenerate(`syntax="proto3";`, (f) => { - f.print(f.jsDoc(`hello world`)); + const lines = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=ts", + generateAny(f) { + f.print(f.jsDoc(`hello world`)); + }, + returnLinesOfFirstFile: true, }); expect(lines).toStrictEqual(["/**", " * hello world", " */"]); }); test("creates JSDoc comment block for message descriptor", async () => { - const lines = await testGenerate( - ` - syntax="proto3"; + const lines = await createTestPluginAndRun({ + proto: ` + syntax="proto3"; message SomeMessage {}; - `, - (f, schema) => { + `, + parameter: "target=ts", + generateAny(f, schema) { f.print(f.jsDoc(schema.files[0].messages[0])); }, - ); + returnLinesOfFirstFile: true, + }); + expect(lines).toStrictEqual([ "/**", " * @generated from message SomeMessage", @@ -45,19 +49,22 @@ describe("file jsDoc", () => { }); test("creates JSDoc comment block for message descriptor with comments", async () => { - const lines = await testGenerate( - ` + const lines = await createTestPluginAndRun({ + proto: ` syntax="proto3"; // discarded detached comment // comment on message message SomeMessage {}; - `, - (f, schema) => { + `, + parameter: "target=ts", + generateAny(f, schema) { f.print(f.jsDoc(schema.files[0].messages[0])); }, - ); + returnLinesOfFirstFile: true, + }); + expect(lines).toStrictEqual([ "/**", " * comment on message", @@ -68,9 +75,15 @@ describe("file jsDoc", () => { }); test("indents", async () => { - const lines = await testGenerate(`syntax="proto3";`, (f) => { - f.print(f.jsDoc(`multi-line\ncomment`, " ")); + const lines = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=ts", + generateAny(f) { + f.print(f.jsDoc(`multi-line\ncomment`, " ")); + }, + returnLinesOfFirstFile: true, }); + expect(lines).toStrictEqual([ " /**", " * multi-line", @@ -80,16 +93,28 @@ describe("file jsDoc", () => { }); test("escapes */", async () => { - const lines = await testGenerate(`syntax="proto3";`, (f) => { - f.print(f.jsDoc(`*/`)); + const lines = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=ts", + generateAny(f) { + f.print(f.jsDoc(`*/`)); + }, + returnLinesOfFirstFile: true, }); + expect(lines).toStrictEqual(["/**", " * *\\/", " */"]); }); test("whitespace is unmodified", async () => { - const lines = await testGenerate(`syntax="proto3";`, (f) => { - f.print(f.jsDoc(`\na\n b\n c\t`)); + const lines = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=ts", + generateAny(f) { + f.print(f.jsDoc(`\na\n b\n c\t`)); + }, + returnLinesOfFirstFile: true, }); + expect(lines).toStrictEqual([ "/**", " *", @@ -99,39 +124,4 @@ describe("file jsDoc", () => { " */", ]); }); - - 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"); - } }); diff --git a/packages/protoplugin-test/src/file-preamble.test.ts b/packages/protoplugin-test/src/file-preamble.test.ts index c69fd6329..afb743b25 100644 --- a/packages/protoplugin-test/src/file-preamble.test.ts +++ b/packages/protoplugin-test/src/file-preamble.test.ts @@ -13,108 +13,101 @@ // 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"; -import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; +import { createTestPluginAndRun } from "./helpers"; describe("file preamble", () => { test("contains plugin name and version", async () => { - const lines = await testGenerate( - { - "x.proto": `syntax="proto3";`, - }, - { - pluginName: "pi-plugin", - pluginVersion: "v3.14159", - }, - ); + const lines = await testGenerate({ + proto: `syntax="proto3";`, + name: "pi-plugin", + version: "v3.14159", + }); expect(lines).toContain("// @generated by pi-plugin v3.14159"); }); test("contains plugin options", async () => { - const lines = await testGenerate( - { - "x.proto": `syntax="proto3";`, - }, - { - parameter: "foo=bar,baz", - }, - ); + const lines = await testGenerate({ + proto: `syntax="proto3";`, + parameter: "foo=bar,baz", + }); expect(lines).toContain( `// @generated by test v1 with parameter "foo=bar,baz"`, ); }); test("elides rewrite_imports plugin option", async () => { - const lines = await testGenerate( - { - "x.proto": `syntax="proto3";`, - }, - { - parameter: - "foo,rewrite_imports=./test/*_pb.js:@buf/test,rewrite_imports=./test/*_web.js:@buf/web,bar", - }, - ); + const lines = await testGenerate({ + proto: `syntax="proto3";`, + parameter: + "foo,rewrite_imports=./test/*_pb.js:@buf/test,rewrite_imports=./test/*_web.js:@buf/web,bar", + }); expect(lines).toContain( `// @generated by test v1 with parameter "foo,bar"`, ); }); - test("contains source file info for proto3", async () => { - const lines = await testGenerate({ - "x.proto": `syntax="proto3";`, - }); - expect(lines).toContain("// @generated from file x.proto (syntax proto3)"); - }); - test("contains eslint-disable annotation", async () => { const lines = await testGenerate({ - "x.proto": `syntax="proto3";`, + proto: `syntax="proto3";`, }); expect(lines).toContain("/* eslint-disable */"); }); test("contains ts-nocheck annotation by default", async () => { const lines = await testGenerate({ - "x.proto": `syntax="proto3";`, + proto: `syntax="proto3";`, }); expect(lines).toContain("// @ts-nocheck"); }); test("does not contain ts-nocheck annotation when turned off", async () => { - const lines = await testGenerate( - { - "x.proto": `syntax="proto3";`, - }, - { - parameter: "ts_nocheck=false", + const lines = await testGenerate({ + proto: `syntax="proto3";`, + parameter: "ts_nocheck=false", + }); + expect(lines).not.toContain("// @ts-nocheck"); + }); + + test("contains source file info for proto3", async () => { + const lines = await testGenerate({ + proto: { + "foo/bar.proto": `syntax="proto3";`, }, + }); + expect(lines).toContain( + "// @generated from file foo/bar.proto (syntax proto3)", ); - expect(lines).not.toContain("// @ts-nocheck"); }); test("contains source file info for proto2", async () => { const lines = await testGenerate({ - "x.proto": `syntax="proto2";`, + proto: { + "foo/bar.proto": `syntax="proto2";`, + }, }); - expect(lines).toContain("// @generated from file x.proto (syntax proto2)"); + expect(lines).toContain( + "// @generated from file foo/bar.proto (syntax proto2)", + ); }); test("contains source file info for edition 2023", async () => { const lines = await testGenerate({ - "x.proto": `edition="2023";`, + proto: { + "foo/bar.proto": `edition="2023";`, + }, }); - expect(lines).toContain("// @generated from file x.proto (edition 2023)"); + expect(lines).toContain( + "// @generated from file foo/bar.proto (edition 2023)", + ); }); test("contains syntax comments", async () => { const lines = await testGenerate({ - "x.proto": ` - // comment above... - // ... the syntax declaration - syntax="proto3"; - `, + proto: ` + // comment above... + // ... the syntax declaration + syntax="proto3"; + `, }); const firstLines = lines.slice( 0, @@ -129,11 +122,11 @@ describe("file preamble", () => { test("contains syntax comments with edition 2023", async () => { const lines = await testGenerate({ - "x.proto": ` + proto: ` // comment above... // ... the syntax declaration edition="2023"; - `, + `, }); const firstLines = lines.slice( 0, @@ -148,13 +141,13 @@ describe("file preamble", () => { test("contains package comments", async () => { const lines = await testGenerate({ - "x.proto": ` + proto: ` syntax="proto3"; // comment above... // ... the package declaration package foo; - `, + `, }); const lastLines = lines.slice(lines.indexOf("// @ts-nocheck")); expect(lastLines).toStrictEqual([ @@ -168,47 +161,24 @@ describe("file preamble", () => { }); // test helper to generate just a file with a preamble for each input proto file - async function testGenerate( - protoFiles: Record, - opt?: { parameter?: string; pluginName?: string; pluginVersion?: string }, - ) { - const plugin = createEcmaScriptPlugin({ - name: opt?.pluginName ?? "test", - version: opt?.pluginVersion ?? "v1", - generateTs: generateFileWithPreamble, - generateJs: generateFileWithPreamble, - generateDts: generateFileWithPreamble, - parseOption: () => { - // accept all options - }, - }); - - function generateFileWithPreamble( - schema: Schema, - target: "js" | "ts" | "dts", - ) { - for (const file of schema.files) { - const f = schema.generateFile(`${file.name}.${target}`); - f.preamble(file); + async function testGenerate(opt: { + proto: string | Record; + parameter?: string; + name?: string; + version?: string; + }) { + return await createTestPluginAndRun({ + ...opt, + generateAny(f, schema) { + f.preamble(schema.files[0]); f.print( "const placeholder = 1; // ensure file is not considered empty", ); - } - } - - const upstream = new UpstreamProtobuf(); - const req = CodeGeneratorRequest.fromBinary( - await upstream.createCodeGeneratorRequest(protoFiles, { - parameter: opt?.parameter, - }), - ); - 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"); + }, + parseOption() { + // accept all options + }, + returnLinesOfFirstFile: true, + }); } }); diff --git a/packages/protoplugin-test/src/file-print.test.ts b/packages/protoplugin-test/src/file-print.test.ts new file mode 100644 index 000000000..8f76502d9 --- /dev/null +++ b/packages/protoplugin-test/src/file-print.test.ts @@ -0,0 +1,264 @@ +// 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 type { GeneratedFile, Schema } from "@bufbuild/protoplugin/ecmascript"; +import { createImportSymbol } from "@bufbuild/protoplugin/ecmascript"; +import { createTestPluginAndRun } from "./helpers"; + +describe("file print", () => { + test("should print bigint literals", async () => { + const lines = await testGenerate((f) => { + f.print(BigInt(123)); + f.print(456n); + }); + expect(lines).toStrictEqual([ + 'import { protoInt64 } from "@bufbuild/protobuf";', + "", + `protoInt64.parse("123")`, + `protoInt64.parse("456")`, + ]); + }); + + test("should print number literals", async () => { + const lines = await testGenerate((f) => { + f.print(123); + }); + expect(lines).toStrictEqual(["123"]); + }); + + test("should print boolean literals", async () => { + const lines = await testGenerate((f) => { + f.print(true); + f.print(false); + }); + expect(lines).toStrictEqual(["true", "false"]); + }); + + test("should print Uint8Array literals", async () => { + const lines = await testGenerate((f) => { + f.print(new Uint8Array()); + f.print(new Uint8Array([0xde, 0xad, 0xbe, 0xef])); + }); + expect(lines).toStrictEqual([ + `new Uint8Array(0)`, + `new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])`, + ]); + }); + + test("should print import symbol", async function () { + const lines = await testGenerate((f) => { + const imp = createImportSymbol("Foo", "bar"); + f.print(imp); + }); + expect(lines).toStrictEqual(['import { Foo } from "bar";', "", "Foo"]); + }); + + test("should print type-only import symbol", async function () { + const lines = await testGenerate((f) => { + const imp = createImportSymbol("Foo", "bar"); + f.print(imp.toTypeOnly()); + }); + expect(lines).toStrictEqual(['import type { Foo } from "bar";', "", "Foo"]); + }); + + test("should print import symbol used as type and value", async function () { + const lines = await testGenerate((f) => { + const imp = createImportSymbol("Foo", "bar"); + f.print(imp); + f.print(imp.toTypeOnly()); + }); + expect(lines).toStrictEqual([ + 'import { Foo } from "bar";', + "", + "Foo", + "Foo", + ]); + }); + + test("should print only one import for the same symbol", async function () { + const lines = await testGenerate((f) => { + const imp = createImportSymbol("Foo", "bar"); + const imp2 = createImportSymbol("Foo", "bar"); + f.print(imp); + f.print(imp2); + }); + expect(lines).toStrictEqual([ + 'import { Foo } from "bar";', + "", + "Foo", + "Foo", + ]); + }); + + test("should escape clashing import symbols", async function () { + const lines = await testGenerate((f) => { + const imp = createImportSymbol("Foo", "a"); + const imp2 = createImportSymbol("Foo", "b"); + f.print(imp); + f.print(imp2); + }); + expect(lines).toStrictEqual([ + `import { Foo } from "a";`, + `import { Foo as Foo$1 } from "b";`, + "", + "Foo", + "Foo$1", + ]); + }); + + test("should escape clashing import symbols with commonjs", async function () { + const lines = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=js,js_import_style=legacy_commonjs", + generateAny(f) { + const imp = createImportSymbol("Foo", "a"); + const imp2 = createImportSymbol("Foo", "b"); + f.print(imp); + f.print(imp2); + }, + returnLinesOfFirstFile: true, + }); + expect(lines).toStrictEqual([ + `"use strict";`, + `Object.defineProperty(exports, "__esModule", { value: true });`, + ``, + `const { Foo } = require("a");`, + `const { Foo: Foo$1 } = require("b");`, + ``, + `Foo`, + `Foo$1`, + ]); + }); + + test("should print runtime imports", async () => { + const lines = await testGenerate((f, schema) => { + f.print(schema.runtime.ScalarType, ".INT32"); + }); + expect(lines).toStrictEqual([ + 'import { ScalarType } from "@bufbuild/protobuf";', + "", + "ScalarType.INT32", + ]); + }); + + test("should print descriptor", async function () { + const lines = await createTestPluginAndRun({ + proto: ` + syntax="proto3"; + message Person {} + `, + parameter: "target=ts", + generateAny(f, schema) { + f.print(schema.files[0].messages[0], ".typeName"); + }, + returnLinesOfFirstFile: true, + }); + expect(lines).toStrictEqual([ + 'import { Person } from "./x_pb.js";', + "", + "Person.typeName", + ]); + }); + + test("should print empty lines", async () => { + const lines = await testGenerate((f) => { + f.print(" "); + f.print(""); + f.print(); + }); + expect(lines).toStrictEqual([" ", "", ""]); + }); + + test("should print multiple printables", async () => { + const lines = await testGenerate((f) => { + f.print("a", "b", "c", 1, " ", createImportSymbol("Foo", "bar")); + }); + expect(lines).toStrictEqual([`import { Foo } from "bar";`, "", "abc1 Foo"]); + }); + + test("should print nested printables", async () => { + const lines = await testGenerate((f) => { + // prettier-ignore + f.print("a", ["b", ["c", "d", [1, " ", createImportSymbol("Foo", "bar")]]]); + }); + expect(lines).toStrictEqual([ + `import { Foo } from "bar";`, + "", + "abcd1 Foo", + ]); + }); + + describe("with tagged template literals", () => { + test("should print empty lines", async () => { + const lines = await testGenerate((f) => { + f.print` `; + f.print``; + }); + expect(lines).toStrictEqual([" ", ""]); + }); + test("should print import symbol", async () => { + const lines = await testGenerate((f) => { + const imp = createImportSymbol("Foo", "bar"); + f.print`${imp}`; + }); + expect(lines).toStrictEqual(['import { Foo } from "bar";', "", "Foo"]); + }); + test("should print real-world import use case", async () => { + const lines = await testGenerate((f) => { + const Foo = createImportSymbol("Foo", "bar"); + f.print`export function foo(): ${Foo.toTypeOnly()} { + return new ${Foo}(); +};`; + }); + expect(lines).toStrictEqual([ + 'import { Foo } from "bar";', + "", + "export function foo(): Foo {", + " return new Foo();", + "};", + ]); + }); + test("should print multiple printables", async () => { + const lines = await testGenerate((f) => { + f.print`${"a"}${"b"}${"c"}${1} ${createImportSymbol("Foo", "bar")}`; + }); + expect(lines).toStrictEqual([ + `import { Foo } from "bar";`, + "", + "abc1 Foo", + ]); + }); + test("should print nested printables", async () => { + const lines = await testGenerate((f) => { + // prettier-ignore + f.print`${"a"}${["b", ["c", "d", [1, " ", createImportSymbol("Foo", "bar")]]]}`; + }); + expect(lines).toStrictEqual([ + `import { Foo } from "bar";`, + "", + "abcd1 Foo", + ]); + }); + }); + + async function testGenerate(opt: (f: GeneratedFile, schema: Schema) => void) { + return createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=ts", + generateAny: opt, + returnLinesOfFirstFile: true, + }); + } +}); diff --git a/packages/protoplugin-test/src/file-string.test.ts b/packages/protoplugin-test/src/file-string.test.ts index 4c4dc452b..cd9340f01 100644 --- a/packages/protoplugin-test/src/file-string.test.ts +++ b/packages/protoplugin-test/src/file-string.test.ts @@ -13,69 +13,51 @@ // limitations under the License. import { describe, expect, test } from "@jest/globals"; -import { CodeGeneratorRequest } from "@bufbuild/protobuf"; -import type { Schema } from "@bufbuild/protoplugin"; -import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; import type { GeneratedFile } from "@bufbuild/protoplugin/ecmascript"; +import { createTestPluginAndRun } from "./helpers"; describe("file string", () => { - test("surrounds string in quotes", () => { - const lines = testGenerate((f) => { + test("surrounds string in quotes", async () => { + const lines = await testGenerate((f) => { f.print("const s = ", f.string("abc"), ";"); }); expect(lines).toStrictEqual([`const s = "abc";`]); }); - test("surrounds string in quotes", () => { - const lines = testGenerate((f) => { + test("surrounds string in quotes", async () => { + const lines = await testGenerate((f) => { f.print(f.string("abc")); }); expect(lines).toStrictEqual([`"abc"`]); }); - test("escapes quote", () => { - const lines = testGenerate((f) => { + test("escapes quote", async () => { + const lines = await testGenerate((f) => { f.print(f.string(`ab"c`)); }); expect(lines).toStrictEqual([`"ab\\"c"`]); }); - test("escapes backslash", () => { - const lines = testGenerate((f) => { + test("escapes backslash", async () => { + const lines = await testGenerate((f) => { f.print(f.string("ab\\c")); }); expect(lines).toStrictEqual([`"ab\\\\c"`]); }); - test("escapes line breaks", () => { - const lines = testGenerate((f) => { + test("escapes line breaks", async () => { + const lines = await testGenerate((f) => { f.print(f.string("ab\r\nc")); }); expect(lines).toStrictEqual([`"ab\\r\\nc"`]); }); - function testGenerate(gen: (f: GeneratedFile) => void) { - const plugin = createEcmaScriptPlugin({ - name: "test", - version: "v1", - generateTs: generateAny, - generateJs: generateAny, - generateDts: generateAny, + async function testGenerate(gen: (f: GeneratedFile) => void) { + return await createTestPluginAndRun({ + proto: `syntax="proto3";`, + generateAny: gen, + parameter: "target=ts", + returnLinesOfFirstFile: true, }); - - function generateAny(schema: Schema) { - gen(schema.generateFile("test.ts")); - } - - const req = new CodeGeneratorRequest({ - parameter: "target=js", - }); - 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"); } }); diff --git a/packages/protoplugin-test/src/gen/proto/custom_options_pb.ts b/packages/protoplugin-test/src/gen/proto/custom_options_pb.ts deleted file mode 100644 index fc76f5b3e..000000000 --- a/packages/protoplugin-test/src/gen/proto/custom_options_pb.ts +++ /dev/null @@ -1,121 +0,0 @@ -// 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. - -// @generated by protoc-gen-es v1.5.1 with parameter "target=ts" -// @generated from file proto/custom_options.proto (package example, syntax proto3) -/* eslint-disable */ -// @ts-nocheck - -import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; -import { Message, proto3 } from "@bufbuild/protobuf"; - -/** - * @generated from enum example.ServiceStatus - */ -export enum ServiceStatus { - /** - * @generated from enum value: UNDEFINED = 0; - */ - UNDEFINED = 0, - - /** - * @generated from enum value: EXPERIMENTAL = 1; - */ - EXPERIMENTAL = 1, - - /** - * @generated from enum value: STABLE = 2; - */ - STABLE = 2, -} -// Retrieve enum metadata with: proto3.getEnumType(ServiceStatus) -proto3.util.setEnumType(ServiceStatus, "example.ServiceStatus", [ - { no: 0, name: "UNDEFINED" }, - { no: 1, name: "EXPERIMENTAL" }, - { no: 2, name: "STABLE" }, -]); - -/** - * @generated from message example.Configuration - */ -export class Configuration extends Message { - /** - * @generated from field: int32 foo = 1; - */ - foo = 0; - - /** - * @generated from field: string bar = 2; - */ - bar = ""; - - /** - * @generated from oneof example.Configuration.qux - */ - qux: { - /** - * @generated from field: string quux = 3; - */ - value: string; - case: "quux"; - } | { case: undefined; value?: undefined } = { case: undefined }; - - /** - * @generated from field: repeated string many = 4; - */ - many: string[] = []; - - /** - * @generated from field: map mapping = 5; - */ - mapping: { [key: string]: string } = {}; - - /** - * @generated from field: string unused = 6; - */ - unused = ""; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "example.Configuration"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "foo", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 2, name: "bar", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 3, name: "quux", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "qux" }, - { no: 4, name: "many", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 5, name: "mapping", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "scalar", T: 9 /* ScalarType.STRING */} }, - { no: 6, name: "unused", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): Configuration { - return new Configuration().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): Configuration { - return new Configuration().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): Configuration { - return new Configuration().fromJsonString(jsonString, options); - } - - static equals(a: Configuration | PlainMessage | undefined, b: Configuration | PlainMessage | undefined): boolean { - return proto3.util.equals(Configuration, a, b); - } -} - diff --git a/packages/protoplugin-test/src/gen/proto/option-enum_pb.ts b/packages/protoplugin-test/src/gen/proto/option-enum_pb.ts new file mode 100644 index 000000000..1e83be0e4 --- /dev/null +++ b/packages/protoplugin-test/src/gen/proto/option-enum_pb.ts @@ -0,0 +1,43 @@ +// 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. + +// @generated by protoc-gen-es v1.5.1 with parameter "target=ts" +// @generated from file proto/option-enum.proto (package test, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { proto3 } from "@bufbuild/protobuf"; + +/** + * Used in custom-options.test.ts + * + * @generated from enum test.OptionEnum + */ +export enum OptionEnum { + /** + * @generated from enum value: OPTION_ENUM_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: OPTION_ENUM_A = 1; + */ + A = 1, +} +// Retrieve enum metadata with: proto3.getEnumType(OptionEnum) +proto3.util.setEnumType(OptionEnum, "test.OptionEnum", [ + { no: 0, name: "OPTION_ENUM_UNSPECIFIED" }, + { no: 1, name: "OPTION_ENUM_A" }, +]); + diff --git a/packages/protoplugin-test/src/gen/proto/address_book_pb.ts b/packages/protoplugin-test/src/gen/proto/option-message_pb.ts similarity index 51% rename from packages/protoplugin-test/src/gen/proto/address_book_pb.ts rename to packages/protoplugin-test/src/gen/proto/option-message_pb.ts index 1a998726c..cf5740dc3 100644 --- a/packages/protoplugin-test/src/gen/proto/address_book_pb.ts +++ b/packages/protoplugin-test/src/gen/proto/option-message_pb.ts @@ -13,50 +13,61 @@ // limitations under the License. // @generated by protoc-gen-es v1.5.1 with parameter "target=ts" -// @generated from file proto/address_book.proto (package example, syntax proto3) +// @generated from file proto/option-message.proto (package test, syntax proto3) /* eslint-disable */ // @ts-nocheck import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; import { Message, proto3 } from "@bufbuild/protobuf"; -import { Person } from "./person_pb.js"; /** - * Our address book file is just one of these. + * Used in custom-options.test.ts * - * @generated from message example.AddressBook + * @generated from message test.OptionMessage */ -export class AddressBook extends Message { +export class OptionMessage extends Message { /** - * @generated from field: repeated example.Person people = 1; + * @generated from field: int32 foo = 1; */ - people: Person[] = []; + foo = 0; - constructor(data?: PartialMessage) { + /** + * @generated from field: string bar = 2; + */ + bar = ""; + + /** + * @generated from field: repeated string many = 4; + */ + many: string[] = []; + + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "example.AddressBook"; + static readonly typeName = "test.OptionMessage"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "people", kind: "message", T: Person, repeated: true }, + { no: 1, name: "foo", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 2, name: "bar", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 4, name: "many", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): AddressBook { - return new AddressBook().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): OptionMessage { + return new OptionMessage().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): AddressBook { - return new AddressBook().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): OptionMessage { + return new OptionMessage().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): AddressBook { - return new AddressBook().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): OptionMessage { + return new OptionMessage().fromJsonString(jsonString, options); } - static equals(a: AddressBook | PlainMessage | undefined, b: AddressBook | PlainMessage | undefined): boolean { - return proto3.util.equals(AddressBook, a, b); + static equals(a: OptionMessage | PlainMessage | undefined, b: OptionMessage | PlainMessage | undefined): boolean { + return proto3.util.equals(OptionMessage, a, b); } } diff --git a/packages/protoplugin-test/src/gen/proto/person_pb.ts b/packages/protoplugin-test/src/gen/proto/person_pb.ts deleted file mode 100644 index 6c768116c..000000000 --- a/packages/protoplugin-test/src/gen/proto/person_pb.ts +++ /dev/null @@ -1,154 +0,0 @@ -// 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. - -// @generated by protoc-gen-es v1.5.1 with parameter "target=ts" -// @generated from file proto/person.proto (package example, syntax proto3) -/* eslint-disable */ -// @ts-nocheck - -import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; -import { Message, proto3, Timestamp } from "@bufbuild/protobuf"; - -/** - * @generated from message example.Person - */ -export class Person extends Message { - /** - * @generated from field: string name = 1; - */ - name = ""; - - /** - * Unique ID number for this person. - * - * @generated from field: int32 id = 2; - */ - id = 0; - - /** - * @generated from field: string email = 3; - */ - email = ""; - - /** - * @generated from field: repeated example.Person.PhoneNumber phones = 4; - */ - phones: Person_PhoneNumber[] = []; - - /** - * @generated from field: google.protobuf.Timestamp last_updated = 5; - */ - lastUpdated?: Timestamp; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "example.Person"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "id", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 3, name: "email", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 4, name: "phones", kind: "message", T: Person_PhoneNumber, repeated: true }, - { no: 5, name: "last_updated", kind: "message", T: Timestamp }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): Person { - return new Person().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): Person { - return new Person().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): Person { - return new Person().fromJsonString(jsonString, options); - } - - static equals(a: Person | PlainMessage | undefined, b: Person | PlainMessage | undefined): boolean { - return proto3.util.equals(Person, a, b); - } -} - -/** - * @generated from enum example.Person.PhoneType - */ -export enum Person_PhoneType { - /** - * @generated from enum value: MOBILE = 0; - */ - MOBILE = 0, - - /** - * @generated from enum value: HOME = 1; - */ - HOME = 1, - - /** - * @generated from enum value: WORK = 2; - */ - WORK = 2, -} -// Retrieve enum metadata with: proto3.getEnumType(Person_PhoneType) -proto3.util.setEnumType(Person_PhoneType, "example.Person.PhoneType", [ - { no: 0, name: "MOBILE" }, - { no: 1, name: "HOME" }, - { no: 2, name: "WORK" }, -]); - -/** - * @generated from message example.Person.PhoneNumber - */ -export class Person_PhoneNumber extends Message { - /** - * @generated from field: string number = 1; - */ - number = ""; - - /** - * @generated from field: example.Person.PhoneType type = 2; - */ - type = Person_PhoneType.MOBILE; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "example.Person.PhoneNumber"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "number", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "type", kind: "enum", T: proto3.getEnumType(Person_PhoneType) }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): Person_PhoneNumber { - return new Person_PhoneNumber().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): Person_PhoneNumber { - return new Person_PhoneNumber().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): Person_PhoneNumber { - return new Person_PhoneNumber().fromJsonString(jsonString, options); - } - - static equals(a: Person_PhoneNumber | PlainMessage | undefined, b: Person_PhoneNumber | PlainMessage | undefined): boolean { - return proto3.util.equals(Person_PhoneNumber, a, b); - } -} - diff --git a/packages/protoplugin-test/src/gen/proto/service_pb.ts b/packages/protoplugin-test/src/gen/proto/service_pb.ts deleted file mode 100644 index 8961f0459..000000000 --- a/packages/protoplugin-test/src/gen/proto/service_pb.ts +++ /dev/null @@ -1,183 +0,0 @@ -// 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. - -// @generated by protoc-gen-es v1.5.1 with parameter "target=ts" -// @generated from file proto/service.proto (package example, syntax proto3) -/* eslint-disable */ -// @ts-nocheck - -import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; -import { Message, proto3 } from "@bufbuild/protobuf"; - -/** - * @generated from enum example.EnumWithOptions - */ -export enum EnumWithOptions { - /** - * @generated from enum value: UNSET = 0; - */ - UNSET = 0, - - /** - * @generated from enum value: ACTIVE = 1; - */ - ACTIVE = 1, - - /** - * @generated from enum value: INACTIVE = 2; - */ - INACTIVE = 2, -} -// Retrieve enum metadata with: proto3.getEnumType(EnumWithOptions) -proto3.util.setEnumType(EnumWithOptions, "example.EnumWithOptions", [ - { no: 0, name: "UNSET" }, - { no: 1, name: "ACTIVE" }, - { no: 2, name: "INACTIVE" }, -]); - -/** - * @generated from message example.MessageWithOptions - */ -export class MessageWithOptions extends Message { - /** - * @generated from field: int32 foo = 1; - */ - foo = 0; - - /** - * @generated from field: string bar = 2; - */ - bar = ""; - - /** - * @generated from oneof example.MessageWithOptions.qux - */ - qux: { - /** - * @generated from field: string quux = 3; - */ - value: string; - case: "quux"; - } | { case: undefined; value?: undefined } = { case: undefined }; - - /** - * @generated from field: repeated string many = 4; - */ - many: string[] = []; - - /** - * @generated from field: map mapping = 5; - */ - mapping: { [key: string]: string } = {}; - - /** - * @generated from field: string unused = 6; - */ - unused = ""; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "example.MessageWithOptions"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "foo", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 2, name: "bar", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 3, name: "quux", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "qux" }, - { no: 4, name: "many", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 5, name: "mapping", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "scalar", T: 9 /* ScalarType.STRING */} }, - { no: 6, name: "unused", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): MessageWithOptions { - return new MessageWithOptions().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): MessageWithOptions { - return new MessageWithOptions().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): MessageWithOptions { - return new MessageWithOptions().fromJsonString(jsonString, options); - } - - static equals(a: MessageWithOptions | PlainMessage | undefined, b: MessageWithOptions | PlainMessage | undefined): boolean { - return proto3.util.equals(MessageWithOptions, a, b); - } -} - -/** - * @generated from message example.GetRequest - */ -export class GetRequest extends Message { - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "example.GetRequest"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): GetRequest { - return new GetRequest().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): GetRequest { - return new GetRequest().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): GetRequest { - return new GetRequest().fromJsonString(jsonString, options); - } - - static equals(a: GetRequest | PlainMessage | undefined, b: GetRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetRequest, a, b); - } -} - -/** - * @generated from message example.GetResponse - */ -export class GetResponse extends Message { - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "example.GetResponse"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): GetResponse { - return new GetResponse().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): GetResponse { - return new GetResponse().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): GetResponse { - return new GetResponse().fromJsonString(jsonString, options); - } - - static equals(a: GetResponse | PlainMessage | undefined, b: GetResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetResponse, a, b); - } -} - diff --git a/packages/protoplugin-test/src/generated-file.test.ts b/packages/protoplugin-test/src/generated-file.test.ts deleted file mode 100644 index 3c5ba9b6e..000000000 --- a/packages/protoplugin-test/src/generated-file.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -// 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, it, test } from "@jest/globals"; -import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; -import type { Schema } from "@bufbuild/protoplugin"; -import type { GeneratedFile } from "@bufbuild/protoplugin/ecmascript"; -import { assert, getDescriptorSet } from "./helpers"; -import { CodeGeneratorRequest } from "@bufbuild/protobuf"; - -/** - * Generates a single file using the plugin framework and the given print function. - * Uses descriptorset.bin for the code generator request. - * The returned string array represents the generated file content created using printFn. - */ -function generate( - printFn: (f: GeneratedFile, schema: Schema) => void, -): string[] { - const req = new CodeGeneratorRequest({ parameter: "target=ts" }); - const plugin = createEcmaScriptPlugin({ - name: "test-plugin", - version: "v99.0.0", - generateTs: (schema: Schema) => { - const f = schema.generateFile("test.ts"); - printFn(f, schema); - }, - }); - const res = plugin.run(req); - if (res.file.length !== 1) { - throw new Error(`no file generated`); - } - 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"); -} - -describe("generated file", () => { - it("should print runtime imports", function () { - const lines = generate((f, schema) => { - f.print`${schema.runtime.ScalarType}.INT32`; - }); - expect(lines).toStrictEqual([ - 'import { ScalarType } from "@bufbuild/protobuf";', - "", - "ScalarType.INT32", - ]); - }); - it("should print npm import", function () { - const lines = generate((f) => { - const exampleDesc = getDescriptorSet().messages.get("example.Person"); - assert(exampleDesc); - const Foo = f.import("Foo", "@scope/pkg"); - f.print`${Foo}`; - }); - expect(lines).toStrictEqual([ - 'import { Foo } from "@scope/pkg";', - "", - "Foo", - ]); - }); - it("should print relative import", function () { - const lines = generate((f) => { - const Foo = f.import("Foo", "./foo_zz.js"); - f.print`${Foo}`; - }); - expect(lines).toStrictEqual([ - 'import { Foo } from "./foo_zz.js";', - "", - "Foo", - ]); - }); - it("should print https import", function () { - const lines = generate((f) => { - const Foo = f.import("Foo", "https://example.com/foo.js"); - f.print`${Foo}`; - }); - expect(lines).toStrictEqual([ - 'import { Foo } from "https://example.com/foo.js";', - "", - "Foo", - ]); - }); - it("should print descriptor imports", function () { - const lines = generate((f) => { - const exampleDesc = getDescriptorSet().messages.get("example.Person"); - assert(exampleDesc); - f.print`${exampleDesc}.typeName`; - }); - expect(lines).toStrictEqual([ - 'import { Person } from "./proto/person_pb.js";', - "", - "Person.typeName", - ]); - }); - - describe("print with tagged template literal", function () { - test("one line with symbol", () => { - const lines = generate((f) => { - const Foo = f.import("Foo", "foo"); - f.print`export function foo(): ${Foo} { return new ${Foo}(); };`; - }); - expect(lines).toStrictEqual([ - 'import { Foo } from "foo";', - "", - "export function foo(): Foo { return new Foo(); };", - ]); - }); - test("multi lines with symbol", () => { - const lines = generate((f) => { - const Foo = f.import("Foo", "foo"); - f.print`export function foo(): ${Foo} { - return new ${Foo}(); -};`; - }); - expect(lines).toStrictEqual([ - 'import { Foo } from "foo";', - "", - "export function foo(): Foo {", - " return new Foo();", - "};", - ]); - }); - test("with empty lines", () => { - const lines = generate((f) => { - const Foo = f.import("Foo", "foo"); - f.print` -export function foo(): ${Foo} { - - return new ${Foo}(); -}; -`; - }); - expect(lines).toStrictEqual([ - 'import { Foo } from "foo";', - "", - "", - "export function foo(): Foo {", - "", - " return new Foo();", - "};", - "", - ]); - }); - test("empty literal", () => { - const lines = generate((f) => { - f.print``; - }); - expect(lines).toStrictEqual([""]); - }); - test("with only symbol", () => { - const lines = generate((f) => { - const Foo = f.import("Foo", "foo"); - f.print`${Foo}`; - }); - expect(lines).toStrictEqual(['import { Foo } from "foo";', "", "Foo"]); - }); - }); -}); diff --git a/packages/protoplugin-test/src/helpers.ts b/packages/protoplugin-test/src/helpers.ts index fac61337c..256a9398e 100644 --- a/packages/protoplugin-test/src/helpers.ts +++ b/packages/protoplugin-test/src/helpers.ts @@ -13,49 +13,99 @@ // limitations under the License. import { - createDescriptorSet, - FileDescriptorSet, CodeGeneratorRequest, + CodeGeneratorResponse, } from "@bufbuild/protobuf"; -import { readFileSync } from "fs"; +import type { Plugin } from "@bufbuild/protoplugin"; +import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; +import type { + GeneratedFile, + Schema, + Target, +} from "@bufbuild/protoplugin/ecmascript"; +import { UpstreamProtobuf } from "upstream-protobuf"; +import { expect } from "@jest/globals"; -/** - * Assert that condition is truthy or throw error (with message) - */ -export function assert(condition: unknown, msg?: string): asserts condition { - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions -- we want the implicit conversion to boolean - if (!condition) { - throw new Error(msg); +const upstream = new UpstreamProtobuf(); + +type PluginInit = Parameters[0]; + +// prettier-ignore +type CreateTestPluginAndRunOptions = + { + returnLinesOfFirstFile?: ReturnLinesOfFirstFile; } -} + & + { + proto: string | Record; + parameter?: string; + name?: PluginInit["name"]; + version?: PluginInit["version"]; + parseOption?: PluginInit["parseOption"]; + } + & + ( + { + generateTs: PluginInit["generateTs"]; + generateJs?: PluginInit["generateJs"]; + generateDts?: PluginInit["generateDts"]; + transpile?: PluginInit["transpile"]; + } + | + { generateAny: (f: GeneratedFile, schema: Schema, target: Target) => void; } + ); -/** - * Returns a constructed CodeGeneratorRequest using a pre-built Buf image for testing - */ -export function getCodeGeneratorRequest( - parameter = "", - fileToGenerate: string[], +export async function createTestPluginAndRun( + opt: CreateTestPluginAndRunOptions, +): Promise; +export async function createTestPluginAndRun( + opt: CreateTestPluginAndRunOptions, +): Promise; +export async function createTestPluginAndRun( + opt: CreateTestPluginAndRunOptions, ) { - const fds = getFileDescriptorSet(); - return new CodeGeneratorRequest({ - parameter, - fileToGenerate, // tells the plugin which files from the set to generate - protoFile: fds.file, + const protoFiles = + typeof opt.proto == "string" ? { "x.proto": opt.proto } : opt.proto; + const reqBytes = await upstream.createCodeGeneratorRequest(protoFiles, { + parameter: opt.parameter, }); -} - -/** - * Returns a DescriptorSet from a pre-built Buf image - */ -export function getDescriptorSet() { - const fds = getFileDescriptorSet(); - return createDescriptorSet(fds.file); -} - -/** - * Returns a FileDescriptorSet from a pre-built Buf image - */ -function getFileDescriptorSet() { - const fdsBytes = readFileSync("./descriptorset.bin"); - return FileDescriptorSet.fromBinary(fdsBytes); + const req = CodeGeneratorRequest.fromBinary(reqBytes); + let plugin: Plugin; + const defaultPluginInit = { + name: "test", + version: "v1", + }; + if ("generateAny" in opt) { + plugin = createEcmaScriptPlugin({ + ...defaultPluginInit, + ...opt, + generateTs: (schema: Schema, target: "ts") => { + const f = schema.generateFile("test.ts"); + opt.generateAny(f, schema, target); + }, + generateJs: (schema: Schema, target: "js") => { + const f = schema.generateFile("test.js"); + opt.generateAny(f, schema, target); + }, + generateDts: (schema: Schema, target: "dts") => { + const f = schema.generateFile("test.d.ts"); + opt.generateAny(f, schema, target); + }, + }); + } else { + plugin = createEcmaScriptPlugin({ + ...defaultPluginInit, + ...opt, + }); + } + const res = plugin.run(req); + if (opt.returnLinesOfFirstFile === true) { + 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"); + } + return res; } diff --git a/packages/protoplugin-test/src/import_extension.test.ts b/packages/protoplugin-test/src/import_extension.test.ts index 488e395eb..a5e381f4b 100644 --- a/packages/protoplugin-test/src/import_extension.test.ts +++ b/packages/protoplugin-test/src/import_extension.test.ts @@ -13,97 +13,64 @@ // limitations under the License. import { describe, expect, test } from "@jest/globals"; -import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; -import type { Schema } from "@bufbuild/protoplugin"; -import type { GeneratedFile } from "@bufbuild/protoplugin/ecmascript"; -import { CodeGeneratorRequest } from "@bufbuild/protobuf"; - -/** - * Generates a single file using the plugin framework and the given print function, - * passing the given options to the plugin. - * The returned string array represents the generated file content created using printFn. - */ -function generate( - printFn: (f: GeneratedFile, schema: Schema) => void, - options: string[], -): string[] { - const req = new CodeGeneratorRequest({ - parameter: `target=ts,${options.join(",")}`, - }); - const plugin = createEcmaScriptPlugin({ - name: "test-plugin", - version: "v99.0.0", - generateTs: (schema: Schema) => { - const f = schema.generateFile("test.ts"); - printFn(f, schema); - }, - }); - const res = plugin.run(req); - if (res.file.length !== 1) { - throw new Error(`no file generated`); - } - 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"); -} +import type { GeneratedFile, Schema } from "@bufbuild/protoplugin/ecmascript"; +import { createTestPluginAndRun } from "./helpers"; describe("import_extension", function () { - test("should be replaced with '.ts'", () => { - const lines = generate( - (f) => { - const Bar = f.import("Bar", "./foo/bar_pb.js"); - f.print`${Bar}`; - }, - ["import_extension=.ts"], - ); + test("should be replaced with '.ts'", async () => { + const lines = await testGenerate("target=ts,import_extension=.ts", (f) => { + const Bar = f.import("Bar", "./foo/bar_pb.js"); + f.print`${Bar}`; + }); expect(lines).toStrictEqual([ 'import { Bar } from "./foo/bar_pb.ts";', "", "Bar", ]); }); - test("should be removed with 'none'", () => { - const lines = generate( - (f) => { - const Bar = f.import("Bar", "./foo/bar_pb.js"); - f.print`${Bar}`; - }, - ["import_extension=none"], - ); + test("should be removed with 'none'", async () => { + const lines = await testGenerate("target=ts,import_extension=none", (f) => { + const Bar = f.import("Bar", "./foo/bar_pb.js"); + f.print`${Bar}`; + }); expect(lines).toStrictEqual([ 'import { Bar } from "./foo/bar_pb";', "", "Bar", ]); }); - test("should be removed with ''", () => { - const lines = generate( - (f) => { - const Bar = f.import("Bar", "./foo/bar_pb.js"); - f.print`${Bar}`; - }, - ["import_extension="], - ); + test("should be removed with ''", async () => { + const lines = await testGenerate("target=ts,import_extension=", (f) => { + const Bar = f.import("Bar", "./foo/bar_pb.js"); + f.print`${Bar}`; + }); expect(lines).toStrictEqual([ 'import { Bar } from "./foo/bar_pb";', "", "Bar", ]); }); - test("should only touch .js import paths", () => { - const lines = generate( - (f) => { - const json = f.import("json", "./foo/bar_pb.json"); - f.print`${json}`; - }, - ["import_extension=.ts"], - ); + test("should only touch .js import paths", async () => { + const lines = await testGenerate("target=ts,import_extension=.ts", (f) => { + const json = f.import("json", "./foo/bar_pb.json"); + f.print`${json}`; + }); expect(lines).toStrictEqual([ 'import { json } from "./foo/bar_pb.json";', "", "json", ]); }); + + async function testGenerate( + parameter: string, + gen: (f: GeneratedFile, schema: Schema) => void, + ) { + return await createTestPluginAndRun({ + proto: `syntax="proto3";`, + generateAny: gen, + parameter, + returnLinesOfFirstFile: true, + }); + } }); diff --git a/packages/protoplugin-test/src/js_import_style.test.ts b/packages/protoplugin-test/src/js_import_style.test.ts index 9c5e33d02..731f611ab 100644 --- a/packages/protoplugin-test/src/js_import_style.test.ts +++ b/packages/protoplugin-test/src/js_import_style.test.ts @@ -13,9 +13,8 @@ // limitations under the License. import { describe, expect, test } from "@jest/globals"; -import { CodeGeneratorRequest } from "@bufbuild/protobuf"; -import type { Schema } from "@bufbuild/protoplugin"; -import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; +import type { Schema } from "@bufbuild/protoplugin/ecmascript"; +import { createTestPluginAndRun } from "./helpers"; describe("js_import_style", () => { const linesEsm = [ @@ -27,28 +26,38 @@ describe("js_import_style", () => { `hand();`, ]; describe("unset", () => { - test.each(["js", "ts", "dts"])("uses module with target %p", (target) => { - const lines = testGenerate(`target=${target}`); - expect(lines).toStrictEqual(linesEsm); - }); + test.each(["js", "ts", "dts"])( + "uses module with target %p", + async (target) => { + const lines = await testGenerate(`target=${target}`); + expect(lines).toStrictEqual(linesEsm); + }, + ); }); describe("module", () => { - test.each(["js", "ts", "dts"])("uses module with target %p", (target) => { - const lines = testGenerate(`js_import_style=module,target=${target}`); - expect(lines).toStrictEqual(linesEsm); - }); + test.each(["js", "ts", "dts"])( + "uses module with target %p", + async (target) => { + const lines = await testGenerate( + `js_import_style=module,target=${target}`, + ); + expect(lines).toStrictEqual(linesEsm); + }, + ); }); describe("legacy_commonjs", () => { - test.each(["ts", "dts"])("uses CommonJs with target %p", (target) => { - const lines = testGenerate( + test.each(["ts", "dts"])("uses CommonJs with target %p", async (target) => { + const lines = await testGenerate( `js_import_style=legacy_commonjs,target=${target}`, ); expect(lines).toStrictEqual(linesEsm); }); - test(`uses CommonJs with target "js"`, () => { - const lines = testGenerate(`js_import_style=legacy_commonjs,target=js`); + test(`uses CommonJs with target "js"`, async () => { + const lines = await testGenerate( + `js_import_style=legacy_commonjs,target=js`, + ); expect(lines).toStrictEqual([ `"use strict";`, `Object.defineProperty(exports, "__esModule", { value: true });`, @@ -63,8 +72,8 @@ describe("js_import_style", () => { `exports.MyClass = MyClass;`, ]); }); - test(`uses CommonJs with built-in transpile`, () => { - const lines = testGenerate( + test("uses CommonJs with built-in transpile", async () => { + const lines = await testGenerate( `js_import_style=legacy_commonjs,target=js`, true, ); @@ -83,20 +92,10 @@ describe("js_import_style", () => { }); }); - function testGenerate( + async function testGenerate( parameter: string, useBuiltInTranspileFromTsToJs = false, ) { - const plugin = createEcmaScriptPlugin({ - name: "test", - version: "v1", - generateTs: generateImportAndExportExamples, - generateJs: useBuiltInTranspileFromTsToJs - ? undefined - : generateImportAndExportExamples, - generateDts: generateImportAndExportExamples, - }); - function generateImportAndExportExamples( schema: Schema, target: "js" | "ts" | "dts", @@ -114,15 +113,16 @@ describe("js_import_style", () => { } f.print("hand();"); } - const req = new CodeGeneratorRequest({ + + return await createTestPluginAndRun({ parameter, + proto: `syntax="proto3";`, + generateTs: generateImportAndExportExamples, + generateJs: useBuiltInTranspileFromTsToJs + ? undefined + : generateImportAndExportExamples, + generateDts: generateImportAndExportExamples, + returnLinesOfFirstFile: true, }); - 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"); } }); diff --git a/packages/protoplugin-test/src/keep_empty_files.test.ts b/packages/protoplugin-test/src/keep_empty_files.test.ts index 93e282028..f6c555b37 100644 --- a/packages/protoplugin-test/src/keep_empty_files.test.ts +++ b/packages/protoplugin-test/src/keep_empty_files.test.ts @@ -15,9 +15,8 @@ import { describe, expect, test } from "@jest/globals"; import type { DescFile } from "@bufbuild/protobuf"; import { CodeGeneratorRequest } from "@bufbuild/protobuf"; -import type { Schema } from "@bufbuild/protoplugin"; +import type { GeneratedFile, Schema } from "@bufbuild/protoplugin/ecmascript"; import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; -import type { GeneratedFile } from "@bufbuild/protoplugin/ecmascript"; import { UpstreamProtobuf } from "upstream-protobuf"; describe("keep_empty_files", () => { diff --git a/packages/protoplugin-test/src/custom-plugin-options.test.ts b/packages/protoplugin-test/src/parse-option.test.ts similarity index 98% rename from packages/protoplugin-test/src/custom-plugin-options.test.ts rename to packages/protoplugin-test/src/parse-option.test.ts index 94f65c48d..9f8e687b8 100644 --- a/packages/protoplugin-test/src/custom-plugin-options.test.ts +++ b/packages/protoplugin-test/src/parse-option.test.ts @@ -17,7 +17,7 @@ import { CodeGeneratorRequest } from "@bufbuild/protobuf"; import type { Plugin } from "@bufbuild/protoplugin"; import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; -describe("custom plugin options", () => { +describe("parse custom plugin option", () => { let foo: number | undefined; let bar = false; let baz: string[] = []; diff --git a/packages/protoplugin-test/src/rewrite_imports.test.ts b/packages/protoplugin-test/src/rewrite_imports.test.ts index 6a10a5b10..31e61306c 100644 --- a/packages/protoplugin-test/src/rewrite_imports.test.ts +++ b/packages/protoplugin-test/src/rewrite_imports.test.ts @@ -13,53 +13,18 @@ // limitations under the License. import { describe, expect, test } from "@jest/globals"; -import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; -import type { Schema } from "@bufbuild/protoplugin"; -import type { GeneratedFile } from "@bufbuild/protoplugin/ecmascript"; -import { assert, getDescriptorSet } from "./helpers"; -import { CodeGeneratorRequest } from "@bufbuild/protobuf"; - -/** - * Generates a single file using the plugin framework and the given print function, - * passing the given options to the plugin. - * Uses descriptorset.bin for the code generator request. - * The returned string array represents the generated file content created using printFn. - */ -function generate( - printFn: (f: GeneratedFile, schema: Schema) => void, - options: string[], -): string[] { - const req = new CodeGeneratorRequest({ - parameter: `target=ts,${options.join(",")}`, - }); - const plugin = createEcmaScriptPlugin({ - name: "test-plugin", - version: "v99.0.0", - generateTs: (schema: Schema) => { - const f = schema.generateFile("test.ts"); - printFn(f, schema); - }, - }); - const res = plugin.run(req); - if (res.file.length !== 1) { - throw new Error(`no file generated`); - } - 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"); -} +import type { GeneratedFile, Schema } from "@bufbuild/protoplugin/ecmascript"; +import { createTestPluginAndRun } from "./helpers"; describe("rewrite_imports", function () { - test("example works as documented", () => { - const lines = generate( + test("example works as documented", async () => { + const lines = await testGenerate( + "target=ts,rewrite_imports=./foo/**/*_pb.js:@scope/pkg", (f) => { const Bar = f.import("Bar", "./foo/bar_pb.js"); const Baz = f.import("Baz", "./foo/bar/baz_pb.js"); f.print`console.log(${Bar}, ${Baz});`; }, - ["rewrite_imports=./foo/**/*_pb.js:@scope/pkg"], ); expect(lines).toStrictEqual([ 'import { Bar } from "@scope/pkg/foo/bar_pb.js";', @@ -68,12 +33,12 @@ describe("rewrite_imports", function () { "console.log(Bar, Baz);", ]); }); - test("should rewrite runtime import to other package", () => { - const lines = generate( + test("should rewrite runtime import to other package", async () => { + const lines = await testGenerate( + "target=ts,rewrite_imports=@bufbuild/protobuf:@scope/pkg", (f, schema) => { f.print`${schema.runtime.ScalarType}.INT32`; }, - ["rewrite_imports=@bufbuild/protobuf:@scope/pkg"], ); expect(lines).toStrictEqual([ 'import { ScalarType } from "@scope/pkg";', @@ -81,15 +46,13 @@ describe("rewrite_imports", function () { "ScalarType.INT32", ]); }); - test("should rewrite npm import to other package", () => { - const lines = generate( + test("should rewrite npm import to other package", async () => { + const lines = await testGenerate( + "target=ts,rewrite_imports=@scope/pkg:@other-scope/other-pkg", (f) => { - const exampleDesc = getDescriptorSet().messages.get("example.Person"); - assert(exampleDesc); const Foo = f.import("Foo", "@scope/pkg"); f.print`${Foo}`; }, - ["rewrite_imports=@scope/pkg:@other-scope/other-pkg"], ); expect(lines).toStrictEqual([ 'import { Foo } from "@other-scope/other-pkg";', @@ -97,4 +60,16 @@ describe("rewrite_imports", function () { "Foo", ]); }); + + async function testGenerate( + parameter: string, + gen: (f: GeneratedFile, schema: Schema) => void, + ) { + return await createTestPluginAndRun({ + parameter, + proto: `syntax="proto3";`, + generateAny: gen, + returnLinesOfFirstFile: true, + }); + } }); diff --git a/packages/protoplugin-test/src/target.test.ts b/packages/protoplugin-test/src/target.test.ts new file mode 100644 index 000000000..1db60b9cf --- /dev/null +++ b/packages/protoplugin-test/src/target.test.ts @@ -0,0 +1,234 @@ +// 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 { beforeEach, describe, expect, jest, test } from "@jest/globals"; +import type { FileInfo, Schema } from "@bufbuild/protoplugin/ecmascript"; +import { createTestPluginAndRun } from "./helpers"; +import type { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; +import { CodeGeneratorResponse } from "@bufbuild/protobuf"; + +describe("target", () => { + type PluginInit = Parameters[0]; + let generateTs: jest.Mock; + let generateJs: jest.Mock["generateJs"]>; + let generateDts: jest.Mock["generateDts"]>; + let transpile: jest.Mock["transpile"]>; + + beforeEach(() => { + generateTs = jest.fn((schema: Schema) => + schema.generateFile("test.ts").print(`const foo = "ts";`), + ); + generateJs = jest.fn((schema: Schema) => + schema.generateFile("test.js").print(`const foo = "js";`), + ); + generateDts = jest.fn((schema: Schema) => + schema.generateFile("test.d.ts").print(`declare const foo = "dts";`), + ); + transpile = jest.fn( + ( + files: FileInfo[], + transpileJs: boolean, + transpileDts: boolean, + jsImportStyle: "module" | "legacy_commonjs", + ) => { + const out: FileInfo[] = []; + for (const f of files) { + expect(f.name.endsWith(".ts")).toBeTruthy(); + expect(f.name.endsWith(".d.ts")).toBeFalsy(); + if (transpileJs) { + out.push({ + name: "test.js", + preamble: f.preamble, + content: `const foo = "js transpiled from ts"; // ${jsImportStyle}`, + }); + } + if (transpileDts) { + out.push({ + name: "test.d.ts", + preamble: f.preamble, + content: `declare const foo = "dts transpiled from ts";`, + }); + } + } + return out; + }, + ); + }); + + describe("unset", () => { + test("should generate .js and .d.ts files", async () => { + const res = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "", + generateTs, + generateJs, + generateDts, + transpile, + }); + const gotFiles = res.file.map((f) => f.name ?? "").sort(); + expect(gotFiles).toStrictEqual(["test.js", "test.d.ts"].sort()); + }); + test("should call generateJs and generateDts", async () => { + await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "", + generateTs, + generateJs, + generateDts, + transpile, + }); + expect(generateTs).toBeCalledTimes(0); + expect(generateJs).toBeCalledTimes(1); + expect(generateDts).toBeCalledTimes(1); + expect(transpile).toBeCalledTimes(0); + }); + }); + + const targetCases = [ + "js", + "ts", + "dts", + "js+ts+dts", + "js+ts", + "js+dts", + "ts+dts", + ]; + describe.each(targetCases)("targets %s", (targetsJoined) => { + const targets = targetsJoined.split("+"); + test("should generate expected files", async () => { + const res = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: `target=${targetsJoined}`, + generateTs, + generateJs, + generateDts, + transpile, + }); + const gotFiles = res.file.map((f) => f.name ?? "").sort(); + const wantFiles = targets + .map((t) => (t == "dts" ? "test.d.ts" : `test.${t}`)) + .sort(); + expect(gotFiles).toStrictEqual(wantFiles); + }); + test("should call expected generator functions", async () => { + await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: `target=${targetsJoined}`, + generateTs, + generateJs, + generateDts, + transpile, + }); + expect(generateTs).toBeCalledTimes(targets.includes("ts") ? 1 : 0); + expect(generateJs).toBeCalledTimes(targets.includes("js") ? 1 : 0); + expect(generateDts).toBeCalledTimes(targets.includes("dts") ? 1 : 0); + expect(transpile).toBeCalledTimes(0); + }); + }); + + const transpileCases = [ + { + name: "target js+dts but only generateTs defined", + parameter: "target=js+dts", + definedGenerators: ["ts"], + calledGenerators: ["ts"], + transpileTo: ["js", "dts"], + expectedFiles: ["test.d.ts", "test.js"], + }, + { + name: "target js but only generateTs defined", + parameter: "target=js", + definedGenerators: ["ts"], + calledGenerators: ["ts"], + transpileTo: ["js"], + expectedFiles: ["test.js"], + }, + { + name: "target dts but only generateTs defined", + parameter: "target=dts", + definedGenerators: ["ts"], + calledGenerators: ["ts"], + transpileTo: ["dts"], + expectedFiles: ["test.d.ts"], + }, + { + name: "target js+dts but only generateTs and generateJs defined", + parameter: "target=js+dts", + definedGenerators: ["ts", "js"], + calledGenerators: ["ts", "js"], + transpileTo: ["dts"], + expectedFiles: ["test.d.ts", "test.js"], + }, + { + name: "target js+dts but only generateTs and generateDts defined", + parameter: "target=js+dts", + definedGenerators: ["ts", "dts"], + calledGenerators: ["ts", "dts"], + transpileTo: ["js"], + expectedFiles: ["test.d.ts", "test.js"], + }, + ]; + describe.each(transpileCases)( + "$name", + ({ + parameter, + definedGenerators, + calledGenerators, + transpileTo, + expectedFiles, + }) => { + let res: CodeGeneratorResponse; + beforeEach( + async () => + (res = await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter, + generateTs, + generateJs: definedGenerators.includes("js") ? generateJs : undefined, // prettier-ignore + generateDts: definedGenerators.includes("dts") ? generateDts : undefined, // prettier-ignore + transpile, + })), + ); + + test("should call expected generator functions", () => { + expect(generateTs).toBeCalledTimes( + calledGenerators.includes("ts") ? 1 : 0, + ); + expect(generateJs).toBeCalledTimes(calledGenerators.includes("js") ? 1 : 0); // prettier-ignore + expect(generateDts).toBeCalledTimes(calledGenerators.includes("dts") ? 1 : 0); // prettier-ignore + }); + + test("should call transpile function", () => { + expect(transpile).toBeCalledTimes(1); + expect(transpile).toBeCalledWith( + [ + { + name: "test.ts", + content: `const foo = "ts";\n`, + preamble: undefined, + }, + ], + transpileTo.includes("js"), // transpileJs + transpileTo.includes("dts"), // transpileDts + "module", // jsImportStyle + ); + }); + + test("should generate expected files", () => { + const gotFiles = res.file.map((f) => f.name ?? "").sort(); + expect(gotFiles).toStrictEqual(expectedFiles); + }); + }, + ); +}); diff --git a/packages/protoplugin-test/src/transpile.test.ts b/packages/protoplugin-test/src/transpile.test.ts index 608f9e954..60411f861 100644 --- a/packages/protoplugin-test/src/transpile.test.ts +++ b/packages/protoplugin-test/src/transpile.test.ts @@ -13,130 +13,88 @@ // limitations under the License. import { describe, expect, test } from "@jest/globals"; -import { CodeGeneratorRequest } from "@bufbuild/protobuf"; -import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; -import type { Schema } from "@bufbuild/protoplugin/ecmascript"; +import { createTestPluginAndRun } from "./helpers"; -/** - * Creates a plugin with the given function to generate TypeScript, - * runs the plugin, and returns a function to retrieve output files. - */ -function transpile( - genTs: (schema: Schema) => void, - parameter = "target=ts+js+dts", - parseOption?: (key: string, value: string | undefined) => void, -): (name: string) => string[] { - const req = new CodeGeneratorRequest({ - parameter, - }); - const plugin = createEcmaScriptPlugin({ - name: "test-plugin", - version: "v99.0.0", - generateTs: genTs, - parseOption, - }); - const res = plugin.run(req); - return function linesOf(filename: string): string[] { - const file = res.file.find((f) => f.name === filename); - if (!file) { - throw new Error(`did not find file ${filename}`); - } - let content = file.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"); - }; -} - -describe("transpile", function () { - test("ECMAScript types", () => { - const linesOf = transpile((schema) => { - const f = schema.generateFile("test.ts"); - f.print("export const p = Promise.resolve(true);"); +describe("built-in transpile", () => { + async function testTranspileToDts(linesTs: string[]) { + return await createTestPluginAndRun({ + proto: `syntax="proto3";`, + parameter: "target=dts", + returnLinesOfFirstFile: true, + generateTs: (schema) => { + const f = schema.generateFile("test.ts"); + for (const line of linesTs) { + f.print(line); + } + }, }); - expect(linesOf("test.ts")).toStrictEqual([ - "export const p = Promise.resolve(true);", - ]); - expect(linesOf("test.d.ts")).toStrictEqual([ - "export declare const p: Promise;", - ]); - }); + } - test("TypeScript built-in types", () => { - const linesOf = transpile((schema) => { - const f = schema.generateFile("test.ts"); - f.print("export const n: ReturnType = 1;"); + describe("ECMAScript types", () => { + test("global Promise type transpiles", async () => { + const linesDts = await testTranspileToDts([ + `export const p = Promise.resolve(true);`, + ]); + expect(linesDts).toStrictEqual([ + `export declare const p: Promise;`, + ]); }); - expect(linesOf("test.ts")).toStrictEqual([ - "export const n: ReturnType = 1;", - ]); - expect(linesOf("test.d.ts")).toStrictEqual([ - "export declare const n: ReturnType;", - ]); }); - test("DOM types", () => { - const linesOf = transpile((schema) => { - const f = schema.generateFile("test.ts"); - f.print("export const h = new Headers();"); + describe("TypeScript built-in types", () => { + test("global ReturnType transpiles", async () => { + const linesDts = await testTranspileToDts([ + `export const n: ReturnType = 1;`, + ]); + expect(linesDts).toStrictEqual([ + `export declare const n: ReturnType;`, + ]); }); - expect(linesOf("test.ts")).toStrictEqual([ - "export const h = new Headers();", - ]); - expect(linesOf("test.d.ts")).toStrictEqual([ - "export declare const h: Headers;", - ]); }); - test("runtime types", () => { - const linesOf = transpile((schema) => { - const f = schema.generateFile("test.ts"); - f.print("export const j: ", schema.runtime.JsonValue, " = 1;"); + describe("DOM types", () => { + test("global Headers transpiles", async () => { + const linesDts = await testTranspileToDts([ + `export const h = new Headers();`, + ]); + expect(linesDts).toStrictEqual([`export declare const h: Headers;`]); }); - expect(linesOf("test.ts")).toStrictEqual([ - 'import type { JsonValue } from "@bufbuild/protobuf";', - "", - "export const j: JsonValue = 1;", - ]); - expect(linesOf("test.d.ts")).toStrictEqual([ - 'import type { JsonValue } from "@bufbuild/protobuf";', - "export declare const j: JsonValue;", - ]); }); - test("unknown type is not inferred correctly", () => { - const linesOf = transpile((schema) => { - const f = schema.generateFile("test.ts"); - const Foo = f.import("Foo", "foo"); - f.print("export function foo() { return new ", Foo, "(); };"); + describe("runtime types", () => { + test("JsonValue transpiles", async () => { + const linesDts = await testTranspileToDts([ + `import type { JsonValue } from "@bufbuild/protobuf";`, + `export const j: JsonValue = 1;`, + ]); + expect(linesDts).toStrictEqual([ + `import type { JsonValue } from "@bufbuild/protobuf";`, + `export declare const j: JsonValue;`, + ]); }); - expect(linesOf("test.ts")).toStrictEqual([ - 'import { Foo } from "foo";', - "", - "export function foo() { return new Foo(); };", - ]); - // The return type is inferred as `any` instead of the expected - // `Foo`. This is a limitation of the TypeScript compiler. - expect(linesOf("test.d.ts")).toStrictEqual([ - "export declare function foo(): any;", - ]); }); - test("unknown type can be typed explicitly", () => { - const linesOf = transpile((schema) => { - const f = schema.generateFile("test.ts"); - const Foo = f.import("Foo", "foo"); - f.print("export function foo(): ", Foo, " { return new ", Foo, "(); };"); + describe("unknown type", () => { + test("is not inferred correctly", async () => { + const linesDts = await testTranspileToDts([ + `import { Foo } from "foo";`, + ``, + `export function foo() { return new Foo(); };`, + ]); + // The return type is inferred as `any` instead of the expected + // `Foo`. This is a limitation of the TypeScript compiler. + expect(linesDts).toStrictEqual([`export declare function foo(): any;`]); + }); + test("can be typed explicitly", async () => { + const linesDts = await testTranspileToDts([ + `import { Foo } from "foo";`, + ``, + `export function foo(): Foo { return new Foo(); };`, + ]); + expect(linesDts).toStrictEqual([ + `import { Foo } from "foo";`, + `export declare function foo(): Foo;`, + ]); }); - expect(linesOf("test.ts")).toStrictEqual([ - 'import { Foo } from "foo";', - "", - "export function foo(): Foo { return new Foo(); };", - ]); - expect(linesOf("test.d.ts")).toStrictEqual([ - 'import { Foo } from "foo";', - "export declare function foo(): Foo;", - ]); }); }); diff --git a/packages/protoplugin-test/tsconfig.json b/packages/protoplugin-test/tsconfig.json index d39bb4972..381f161f0 100644 --- a/packages/protoplugin-test/tsconfig.json +++ b/packages/protoplugin-test/tsconfig.json @@ -4,7 +4,7 @@ "compilerOptions": { "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "target": "es2016", + "target": "ES2020", "strict": true, "resolveJsonModule": true }