From c89f712e319e97491566e653a1a8bb439dc2edd3 Mon Sep 17 00:00:00 2001 From: Timo Stamm Date: Mon, 25 Sep 2023 15:53:47 +0200 Subject: [PATCH] Update protoplugin example (#563) --- .nvmrc | 2 +- Makefile | 6 +- docs/writing_plugins.md | 7 +- packages/protoplugin-example/README.md | 54 +++++++++------ packages/protoplugin-example/buf.gen.yaml | 4 +- packages/protoplugin-example/package.json | 8 +-- .../protoplugin-example/test/generated.ts | 67 +++++++++++++++++++ packages/protoplugin-example/tsconfig.json | 8 ++- 8 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 packages/protoplugin-example/test/generated.ts diff --git a/.nvmrc b/.nvmrc index e048c8ca1..6aab9b43f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.15.0 +v18.18.0 diff --git a/Makefile b/Makefile index 2b2eb6821..ea6cc7bdf 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,7 @@ $(BUILD)/protoplugin-test: $(BUILD)/protoplugin $(GEN)/protoplugin-test node_mod $(BUILD)/protoplugin-example: $(BUILD)/protoc-gen-es packages/protoplugin-example/buf.gen.yaml node_modules tsconfig.base.json packages/protoplugin-example/tsconfig.json $(shell find packages/protoplugin-example/src -name '*.ts') npm run -w packages/protoplugin-example clean - npx -w packages/protoplugin-example buf generate buf.build/connectrpc/eliza + npm run -w packages/protoplugin-example generate npm run -w packages/protoplugin-example build @mkdir -p $(@D) @touch $(@) @@ -173,6 +173,10 @@ test-protobuf: $(BUILD)/protobuf-test packages/protobuf-test/jest.config.js test-protoplugin: $(BUILD)/protoplugin-test packages/protoplugin-test/jest.config.js npm run -w packages/protoplugin-test test +.PHONY: test-protoplugin-example +test-protoplugin-example: $(BUILD)/protoplugin-example + npm run -w packages/protoplugin-example test + .PHONY: test-conformance test-conformance: $(BIN)/conformance_test_runner $(BUILD)/protobuf-conformance cd packages/protobuf-conformance \ diff --git a/docs/writing_plugins.md b/docs/writing_plugins.md index 1ac701e86..86e53701b 100644 --- a/docs/writing_plugins.md +++ b/docs/writing_plugins.md @@ -535,9 +535,12 @@ const enumVal: FooEnum | undefined = findCustomEnumOption(descMessage, 50001); ## Testing -There is no specific formula for how to test an individual plugin. The official [protoc-gen-es](../packages/protoc-gen-es) plugin is extensively tested and could provide some guidance. In addition, there are examples of testing the framework in the [protoplugin-test package](../packages/protoplugin-test). +We recommend to test generated code just like handwritten code. Identify a +representative protobuf file for your use case, generate code, and then simply +run tests against the generated code. -A helpful suggestion is to generate specific use cases that are expected for your plugin and then test that the output is what is expected. It is a bit difficult to test discrete functionality so verifying the output is valid is the recommended approach. To test the transpilation process specifically, it may be helpful to generate your own JavaScript and declaration files and then verify that they match transpilation. +If you implement your own generator functions for the `js` and `dts` targets, +we recommend to run all tests against both. ## Examples diff --git a/packages/protoplugin-example/README.md b/packages/protoplugin-example/README.md index 2dbaea287..1b2873535 100644 --- a/packages/protoplugin-example/README.md +++ b/packages/protoplugin-example/README.md @@ -1,36 +1,50 @@ # Protoplugin Example -This directory contains an example plugin, which shows how to work with the -plugin framework. It also contains a separate webpage which shows the generated files working with a remote server. +This example shows how to write a custom plugin. We generate [Twirp](https://twitchtv.github.io/twirp/docs/spec_v7.html) +clients from service definitions in Protobuf files. -The code generation logic for the actual plugin is located in [`protoc-gen-twirp-es.ts`](src/protoc-gen-twirp-es.ts). -The sample plugin generates a [Twirp](https://twitchtv.github.io/twirp/docs/spec_v7.html) client from service -definitions in Protobuf files. The Twirp client uses base types generated from -[`@bufbuild/protobuf-es`](https://www.npmjs.com/package/@bufbuild/protoc-gen-es). +## Run the example -From the project root, first install and build all required packages: +You will need [Node](https://nodejs.org/en/download/) in version 18.17.0 or later installed. +Download the example project and install its dependencies: ```shell -npm install -w packages/protoplugin-example -npm run -w packages/protobuf build -npm run -w packages/protoplugin build -npm run -w packages/protoc-gen-es build +curl -L https://github.com/bufbuild/protobuf-es/archive/refs/heads/main.zip > protobuf-es-main.zip +unzip protobuf-es-main.zip 'protobuf-es-main/packages/protoplugin-example/*' + +cd protobuf-es-main/packages/protoplugin-example +npm install +``` + +To see the client in action: + +```shell +npm start +``` + +Open http://127.0.0.1:3000/ in your browser. + + +To re-generate code: + +```shell +npx buf generate buf.build/connectrpc/eliza ``` -Next, `cd` into the example directory and build: +This will generate the [Eliza module](https://buf.build/connectrpc/eliza) from the Buf Schema Registry (BSR). +You can change this path to generate additional files locally or from the BSR. + +Test the generated code: ```shell -cd packages/protoplugin-example -npm run build +npm test ``` -To run the plugin (i.e. generate files), use the following command. This will generate files based on the -[Eliza module](https://buf.build/connectrpc/eliza) in the Buf Schema Registry (BSR). You can change this path to generate -additional files locally or from the BSR. +## About this example -`npx buf generate buf.build/connectrpc/eliza` +This example is a starting point - we encourage you to try it out and experiment. -To run the example webpage and see the generated code in action: +Take a look at the code generation logic in [protoc-gen-twirp-es.ts](./src/protoc-gen-twirp-es.ts), +and at [buf.gen.yaml](./buf.gen.yaml) for how it is invoked. -`npm run start` diff --git a/packages/protoplugin-example/buf.gen.yaml b/packages/protoplugin-example/buf.gen.yaml index 93cfea5f8..ae8043869 100644 --- a/packages/protoplugin-example/buf.gen.yaml +++ b/packages/protoplugin-example/buf.gen.yaml @@ -8,6 +8,8 @@ plugins: opt: target=ts out: src/gen - plugin: twirp-es - path: ./src/protoc-gen-twirp-es.ts + # Override the path to the plugin binary. + # See https://buf.build/docs/configuration/v1/buf-gen-yaml#path + path: ["tsx", "./src/protoc-gen-twirp-es.ts"] opt: target=ts out: src/gen diff --git a/packages/protoplugin-example/package.json b/packages/protoplugin-example/package.json index 4d439c8e6..506d3ec95 100644 --- a/packages/protoplugin-example/package.json +++ b/packages/protoplugin-example/package.json @@ -6,13 +6,11 @@ "scripts": { "clean": "rm -rf src/gen", "build": "../../node_modules/typescript/bin/tsc --noEmit", - "start": "npx esbuild src/index.ts --serve=localhost:3000 --servedir=www --outdir=www --bundle --global-name=eliza" + "start": "npx esbuild src/index.ts --serve=localhost:3000 --servedir=www --outdir=www --bundle --global-name=eliza", + "test": "node --loader tsx --test test/*.ts", + "generate": "buf generate buf.build/connectrpc/eliza" }, - "author": "Buf", "license": "Apache-2.0", - "engines": { - "node": ">=14" - }, "dependencies": { "@bufbuild/buf": "^1.25.0", "@bufbuild/protobuf": "^1.3.1", diff --git a/packages/protoplugin-example/test/generated.ts b/packages/protoplugin-example/test/generated.ts new file mode 100644 index 000000000..4029fa66b --- /dev/null +++ b/packages/protoplugin-example/test/generated.ts @@ -0,0 +1,67 @@ +// 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 * as assert from "node:assert/strict"; +import { describe, it, mock } from "node:test"; +import { ElizaServiceClient } from "../src/gen/connectrpc/eliza/v1/eliza_twirp"; +import { SayRequest } from "../src/gen/connectrpc/eliza/v1/eliza_pb"; + +describe("custom plugin", async () => { + it("should generate client class", () => { + assert.equal(typeof ElizaServiceClient, "function"); + const client = new ElizaServiceClient("https://example.com"); + assert.ok(client !== undefined); + }); + describe("generated client", () => { + it("should should take argument in constructor", () => { + const client = new ElizaServiceClient("https://example.com"); + assert.ok(client !== undefined); + assert.equal( + (client as unknown as Record).baseUrl, + "https://example.com", + ); + }); + it("should have method for unary RPC", () => { + const client = new ElizaServiceClient("https://example.com"); + assert.equal(typeof client.say, "function"); + }); + it("should use fetch", async (t) => { + let fetch = mock.fn(globalThis.fetch); + globalThis.fetch = fetch; + t.after(() => fetch.mock.restore()); + fetch.mock.mockImplementationOnce( + async () => + new Response('{"sentence":"ho"}', { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + const client = new ElizaServiceClient("https://example.com"); + const res = await client.say(new SayRequest({ sentence: "hi" })); + assert.equal(res.sentence, "ho"); + assert.equal(fetch.mock.callCount(), 1); + const [argInput, argInit] = fetch.mock.calls[0].arguments; + assert.strictEqual( + argInput, + "https://example.com/connectrpc.eliza.v1.ElizaService/Say", + ); + assert.equal(argInit?.method, "POST"); + assert.equal( + new Headers(argInit?.headers).get("Content-Type"), + "application/json", + ); + assert.equal(argInit?.body, '{"sentence":"hi"}'); + }); + }); +}); diff --git a/packages/protoplugin-example/tsconfig.json b/packages/protoplugin-example/tsconfig.json index c6edeaabc..b4a565e9a 100644 --- a/packages/protoplugin-example/tsconfig.json +++ b/packages/protoplugin-example/tsconfig.json @@ -1,8 +1,12 @@ { - "files": ["src/protoc-gen-twirp-es.ts", "src/index.ts"], - "extends": "../../tsconfig.base.json", + "include": ["src/**/*.ts", "test/*.ts"], "compilerOptions": { + "target": "es2017", + "moduleResolution": "Node", + "esModuleInterop": false, "resolveJsonModule": true, + "strict": true, + "forceConsistentCasingInFileNames": true, "verbatimModuleSyntax": true } }