diff --git a/cli/src/commands/router/commands/plugin/templates/go.ts b/cli/src/commands/router/commands/plugin/templates/go.ts index dd850bde73..06e8dda400 100644 --- a/cli/src/commands/router/commands/plugin/templates/go.ts +++ b/cli/src/commands/router/commands/plugin/templates/go.ts @@ -3,22 +3,22 @@ /* eslint-disable no-template-curly-in-string */ const dockerfile = - 'FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder\n\n# Multi-platform build arguments\nARG TARGETOS\nARG TARGETARCH\n\nWORKDIR /build\n\n# Copy go mod files\nCOPY go.mod go.sum ./\nRUN go mod download\n\n# Copy source code\nCOPY . .\n\nRUN --mount=type=cache,target="/root/.cache/go-build" CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o dist/plugin ./src\n\nFROM --platform=$BUILDPLATFORM scratch\n\nCOPY --from=builder /build/dist/plugin ./{originalPluginName}-plugin\n\nENTRYPOINT ["./{originalPluginName}-plugin"]\n'; + 'FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder\r\n\r\n# Multi-platform build arguments\r\nARG TARGETOS\r\nARG TARGETARCH\r\n\r\nWORKDIR /build\r\n\r\n# Copy go mod files\r\nCOPY go.mod go.sum ./\r\nRUN go mod download\r\n\r\n# Copy source code\r\nCOPY . .\r\n\r\nRUN --mount=type=cache,target="/root/.cache/go-build" CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o dist/plugin ./src\r\n\r\nFROM --platform=$BUILDPLATFORM scratch\r\n\r\nCOPY --from=builder /build/dist/plugin ./{originalPluginName}-plugin\r\n\r\nENTRYPOINT ["./{originalPluginName}-plugin"]\r\n'; const cursorRules = - '---\ndescription: {name} Plugin Guide\nglobs: src/**\nalwaysApply: false\n---\n\n# {name} Plugin Development Guide\n\nYou are an expert in developing Cosmo Router plugins. You are given a GraphQL schema, and you need to implement the Go code for the plugin.\nYour goal is to implement the plugin in a way that is easy to understand and maintain. You add tests to ensure the plugin works as expected.\n\nAll make commands need to be run from the plugin directory `{pluginDir}`.\n\n## Plugin Structure\n\nA plugin is structured as follows:\n\n```\nplugins/{originalPluginName}/\n├── Makefile # Build automation\n├── go.mod # Go module definition\n├── go.sum # Go module checksums\n├── src/\n│ ├── schema.graphql # GraphQL schema (API contract)\n│ ├── main.go # Plugin implementation\n│ └── main_test.go # Tests for the plugin\n├── generated/ # Auto-generated files (DO NOT EDIT)\n│ ├── service.proto # Generated Protocol Buffers\n│ ├── service.pb.go # Generated Go structures\n│ ├── service.proto.lock.json # Generated Protobuf lock file\n│ └── service_grpc.pb.go # Generated gRPC service\n└── bin/ # Compiled binaries\n └── plugin # The compiled plugin binary\n```\n\n## Development Workflow\n\n1. When modifying the GraphQL schema in `src/schema.graphql`, you need to regenerate the code with `make generate`.\n2. Look into the generated code in `generated/service.proto` and `generated/service.pb.go` to understand the updated API contract and service methods.\n3. Implement the new RPC methods in `src/main.go`.\n4. Add tests to `src/main_test.go` to ensure the plugin works as expected. You need to run `make test` to ensure the tests pass.\n5. Finally, build the plugin with `make build` to ensure the plugin is working as expected.\n6. Your job is done after successfully building the plugin. Don\'t verify if the binary was created. The build command will take care of that.\n\n**Important**: Never manipulate the files inside `generated` directory yourself. Don\'t touch the `service.proto`, `service.proto.lock.json`, `service.pb.go` and `service_grpc.pb.go` files.\n\nYou can update the Go dependencies by running `make test` to ensure the dependencies are up to date. It runs `go mod tidy` under the hood.\n\n## Implementation Pattern\n\n### Service Integration\n\nIf you need to integrate with other HTTP services, you should prefer to use the `github.com/wundergraph/cosmo/router-plugin/httpclient` package.\nAlways prefer a real integration over mocking. In the tests, you can mock the external service by bootstrapping an http server that returns the expected response.\nIn tests, focus on a well-defined contract and the expected behavior of your service. Structure tests by endpoint, use-cases and use table-driven tests when possible.\n\nHere is an example of how to use the `httpclient` package:\n\n```go\n// Initialize HTTP client for external API calls\n// The base URL is the URL of the external API\nclient := httpclient.New(\n httpclient.WithBaseURL(""),\n httpclient.WithTimeout(5*time.Second),\n httpclient.WithHeaders(map[string]string{}),\n)\n// A HTTP GET request to the external API\nresp, err := client.Get(ctx, "/")\n// A HTTP POST/PUT/DELETE request to the external API with a struct that is marshalled to JSON\nresp, err := client.Post(ctx, "/", payload)\n// Passing payload with custom request options\nresp, err := client.Put(ctx, "/", payload,\n httpclient.WithHeaders(map[string]string{}),\n)\n// Unmarshal the JSON response into our data structure\ndata, err := httpclient.UnmarshalTo[[]ResponseType](resp)\n// The response offers the following fields:\ntype Response struct {\n\tStatusCode int\n\tHeaders http.Header\n\tBody []byte\n}\n// You can check for success (StatusCode >= 200 && StatusCode < 300)\nresp.IsSuccess()\n```\n'; + '---\r\ndescription: {name} Plugin Guide\r\nglobs: src/**\r\nalwaysApply: false\r\n---\r\n\r\n# {name} Plugin Development Guide\r\n\r\nYou are an expert in developing Cosmo Router plugins. You are given a GraphQL schema, and you need to implement the Go code for the plugin.\r\nYour goal is to implement the plugin in a way that is easy to understand and maintain. You add tests to ensure the plugin works as expected.\r\n\r\nAll make commands need to be run from the plugin directory `{pluginDir}`.\r\n\r\n## Plugin Structure\r\n\r\nA plugin is structured as follows:\r\n\r\n```\r\nplugins/{originalPluginName}/\r\n├── Makefile # Build automation\r\n├── go.mod # Go module definition\r\n├── go.sum # Go module checksums\r\n├── src/\r\n│ ├── schema.graphql # GraphQL schema (API contract)\r\n│ ├── main.go # Plugin implementation\r\n│ └── main_test.go # Tests for the plugin\r\n├── generated/ # Auto-generated files (DO NOT EDIT)\r\n│ ├── service.proto # Generated Protocol Buffers\r\n│ ├── service.pb.go # Generated Go structures\r\n│ ├── service.proto.lock.json # Generated Protobuf lock file\r\n│ └── service_grpc.pb.go # Generated gRPC service\r\n└── bin/ # Compiled binaries\r\n └── plugin # The compiled plugin binary\r\n```\r\n\r\n## Development Workflow\r\n\r\n1. When modifying the GraphQL schema in `src/schema.graphql`, you need to regenerate the code with `make generate`.\r\n2. Look into the generated code in `generated/service.proto` and `generated/service.pb.go` to understand the updated API contract and service methods.\r\n3. Implement the new RPC methods in `src/main.go`.\r\n4. Add tests to `src/main_test.go` to ensure the plugin works as expected. You need to run `make test` to ensure the tests pass.\r\n5. Finally, build the plugin with `make build` to ensure the plugin is working as expected.\r\n6. Your job is done after successfully building the plugin. Don\'t verify if the binary was created. The build command will take care of that.\r\n\r\n**Important**: Never manipulate the files inside `generated` directory yourself. Don\'t touch the `service.proto`, `service.proto.lock.json`, `service.pb.go` and `service_grpc.pb.go` files.\r\n\r\nYou can update the Go dependencies by running `make test` to ensure the dependencies are up to date. It runs `go mod tidy` under the hood.\r\n\r\n## Implementation Pattern\r\n\r\n### Service Integration\r\n\r\nIf you need to integrate with other HTTP services, you should prefer to use the `github.com/wundergraph/cosmo/router-plugin/httpclient` package.\r\nAlways prefer a real integration over mocking. In the tests, you can mock the external service by bootstrapping an http server that returns the expected response.\r\nIn tests, focus on a well-defined contract and the expected behavior of your service. Structure tests by endpoint, use-cases and use table-driven tests when possible.\r\n\r\nHere is an example of how to use the `httpclient` package:\r\n\r\n```go\r\n// Initialize HTTP client for external API calls\r\n// The base URL is the URL of the external API\r\nclient := httpclient.New(\r\n httpclient.WithBaseURL(""),\r\n httpclient.WithTimeout(5*time.Second),\r\n httpclient.WithHeaders(map[string]string{}),\r\n)\r\n// A HTTP GET request to the external API\r\nresp, err := client.Get(ctx, "/")\r\n// A HTTP POST/PUT/DELETE request to the external API with a struct that is marshalled to JSON\r\nresp, err := client.Post(ctx, "/", payload)\r\n// Passing payload with custom request options\r\nresp, err := client.Put(ctx, "/", payload,\r\n httpclient.WithHeaders(map[string]string{}),\r\n)\r\n// Unmarshal the JSON response into our data structure\r\ndata, err := httpclient.UnmarshalTo[[]ResponseType](resp)\r\n// The response offers the following fields:\r\ntype Response struct {\r\n\tStatusCode int\r\n\tHeaders http.Header\r\n\tBody []byte\r\n}\r\n// You can check for success (StatusCode >= 200 && StatusCode < 300)\r\nresp.IsSuccess()\r\n```\r\n'; const goMod = - '\nmodule {modulePath}\n\ngo 1.25.1\n\nrequire (\n github.com/stretchr/testify v1.10.0\n github.com/wundergraph/cosmo/router-plugin v0.0.0-20250824152218-8eebc34c4995 // v0.4.1\n google.golang.org/grpc v1.68.1\n google.golang.org/protobuf v1.36.5\n)\n'; + '\r\nmodule {modulePath}\r\n\r\ngo 1.25.1\r\n\r\nrequire (\r\n github.com/stretchr/testify v1.10.0\r\n github.com/wundergraph/cosmo/router-plugin v0.0.0-20250824152218-8eebc34c4995 // v0.4.1\r\n google.golang.org/grpc v1.68.1\r\n google.golang.org/protobuf v1.36.5\r\n)\r\n'; const mainGo = - 'package main\n\nimport (\n "context"\n "log"\n "strconv"\n\n service "github.com/wundergraph/cosmo/plugin/generated"\n\n routerplugin "github.com/wundergraph/cosmo/router-plugin"\n "google.golang.org/grpc"\n)\n\nfunc main() {\n pl, err := routerplugin.NewRouterPlugin(func(s *grpc.Server) {\n s.RegisterService(&service.{serviceName}_ServiceDesc, &{serviceName}{\n nextID: 1,\n })\n }, routerplugin.WithTracing())\n\n if err != nil {\n log.Fatalf("failed to create router plugin: %v", err)\n }\n\n pl.Serve()\n}\n\ntype {serviceName} struct {\n service.Unimplemented{serviceName}Server\n nextID int\n}\n\nfunc (s *{serviceName}) QueryHello(ctx context.Context, req *service.QueryHelloRequest) (*service.QueryHelloResponse, error) {\n response := &service.QueryHelloResponse{\n Hello: &service.World{\n Id: strconv.Itoa(s.nextID),\n Name: req.Name,\n },\n }\n s.nextID++\n return response, nil\n}\n'; + 'package main\r\n\r\nimport (\r\n "context"\r\n "log"\r\n "strconv"\r\n\r\n service "github.com/wundergraph/cosmo/plugin/generated"\r\n\r\n routerplugin "github.com/wundergraph/cosmo/router-plugin"\r\n "google.golang.org/grpc"\r\n)\r\n\r\nfunc main() {\r\n pl, err := routerplugin.NewRouterPlugin(func(s *grpc.Server) {\r\n s.RegisterService(&service.{serviceName}_ServiceDesc, &{serviceName}{\r\n nextID: 1,\r\n })\r\n }, routerplugin.WithTracing())\r\n\r\n if err != nil {\r\n log.Fatalf("failed to create router plugin: %v", err)\r\n }\r\n\r\n pl.Serve()\r\n}\r\n\r\ntype {serviceName} struct {\r\n service.Unimplemented{serviceName}Server\r\n nextID int\r\n}\r\n\r\nfunc (s *{serviceName}) QueryHello(ctx context.Context, req *service.QueryHelloRequest) (*service.QueryHelloResponse, error) {\r\n response := &service.QueryHelloResponse{\r\n Hello: &service.World{\r\n Id: strconv.Itoa(s.nextID),\r\n Name: req.Name,\r\n },\r\n }\r\n s.nextID++\r\n return response, nil\r\n}\r\n'; const mainTestGo = - 'package main\n\nimport (\n "context"\n "net"\n "testing"\n\n "github.com/stretchr/testify/assert"\n "github.com/stretchr/testify/require"\n service "github.com/wundergraph/cosmo/plugin/generated"\n "google.golang.org/grpc"\n "google.golang.org/grpc/credentials/insecure"\n "google.golang.org/grpc/test/bufconn"\n)\n\nconst bufSize = 1024 * 1024\n\n// testService is a wrapper that holds the gRPC test components\ntype testService struct {\n grpcConn *grpc.ClientConn\n client service.{serviceName}Client\n cleanup func()\n}\n\n// setupTestService creates a local gRPC server for testing\nfunc setupTestService(t *testing.T) *testService {\n // Create a buffer for gRPC connections\n lis := bufconn.Listen(bufSize)\n\n // Create a new gRPC server\n grpcServer := grpc.NewServer()\n\n // Register our service\n service.Register{serviceName}Server(grpcServer, &{serviceName}{\n nextID: 1,\n })\n\n // Start the server\n go func() {\n if err := grpcServer.Serve(lis); err != nil {\n t.Fatalf("failed to serve: %v", err)\n }\n }()\n\n // Create a client connection\n dialer := func(context.Context, string) (net.Conn, error) {\n return lis.Dial()\n }\n conn, err := grpc.Dial(\n "passthrough:///bufnet",\n grpc.WithContextDialer(dialer),\n grpc.WithTransportCredentials(insecure.NewCredentials()),\n )\n require.NoError(t, err)\n\n // Create the service client\n client := service.New{serviceName}Client(conn)\n\n // Return cleanup function\n cleanup := func() {\n conn.Close()\n grpcServer.Stop()\n }\n\n return &testService{\n grpcConn: conn,\n client: client,\n cleanup: cleanup,\n }\n}\n\nfunc TestQueryHello(t *testing.T) {\n // Set up basic service\n svc := setupTestService(t)\n defer svc.cleanup()\n\n tests := []struct {\n name string\n userName string\n wantId string\n wantName string\n wantErr bool\n }{\n {\n name: "valid hello",\n userName: "Alice",\n wantId: "1",\n wantName: "Alice",\n wantErr: false,\n },\n {\n name: "empty name",\n userName: "",\n wantId: "2",\n wantName: "", // Empty name should be preserved\n wantErr: false,\n },\n {\n name: "special characters",\n userName: "John & Jane",\n wantId: "3",\n wantName: "John & Jane",\n wantErr: false,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n req := &service.QueryHelloRequest{\n Name: tt.userName,\n }\n\n resp, err := svc.client.QueryHello(context.Background(), req)\n if tt.wantErr {\n assert.Error(t, err)\n return\n }\n\n assert.NoError(t, err)\n assert.NotNil(t, resp.Hello)\n assert.Equal(t, tt.wantId, resp.Hello.Id)\n assert.Equal(t, tt.wantName, resp.Hello.Name)\n })\n }\n}\n\nfunc TestSequentialIDs(t *testing.T) {\n // Set up basic service\n svc := setupTestService(t)\n defer svc.cleanup()\n\n // The first request should get ID "1"\n firstReq := &service.QueryHelloRequest{Name: "First"}\n firstResp, err := svc.client.QueryHello(context.Background(), firstReq)\n require.NoError(t, err)\n assert.Equal(t, "1", firstResp.Hello.Id)\n\n // The second request should get ID "2"\n secondReq := &service.QueryHelloRequest{Name: "Second"}\n secondResp, err := svc.client.QueryHello(context.Background(), secondReq)\n require.NoError(t, err)\n assert.Equal(t, "2", secondResp.Hello.Id)\n\n // The third request should get ID "3"\n thirdReq := &service.QueryHelloRequest{Name: "Third"}\n thirdResp, err := svc.client.QueryHello(context.Background(), thirdReq)\n require.NoError(t, err)\n assert.Equal(t, "3", thirdResp.Hello.Id)\n}\n'; + 'package main\r\n\r\nimport (\r\n "context"\r\n "net"\r\n "testing"\r\n\r\n "github.com/stretchr/testify/assert"\r\n "github.com/stretchr/testify/require"\r\n service "github.com/wundergraph/cosmo/plugin/generated"\r\n "google.golang.org/grpc"\r\n "google.golang.org/grpc/credentials/insecure"\r\n "google.golang.org/grpc/test/bufconn"\r\n)\r\n\r\nconst bufSize = 1024 * 1024\r\n\r\n// testService is a wrapper that holds the gRPC test components\r\ntype testService struct {\r\n grpcConn *grpc.ClientConn\r\n client service.{serviceName}Client\r\n cleanup func()\r\n}\r\n\r\n// setupTestService creates a local gRPC server for testing\r\nfunc setupTestService(t *testing.T) *testService {\r\n // Create a buffer for gRPC connections\r\n lis := bufconn.Listen(bufSize)\r\n\r\n // Create a new gRPC server\r\n grpcServer := grpc.NewServer()\r\n\r\n // Register our service\r\n service.Register{serviceName}Server(grpcServer, &{serviceName}{\r\n nextID: 1,\r\n })\r\n\r\n // Start the server\r\n go func() {\r\n if err := grpcServer.Serve(lis); err != nil {\r\n t.Fatalf("failed to serve: %v", err)\r\n }\r\n }()\r\n\r\n // Create a client connection\r\n dialer := func(context.Context, string) (net.Conn, error) {\r\n return lis.Dial()\r\n }\r\n conn, err := grpc.Dial(\r\n "passthrough:///bufnet",\r\n grpc.WithContextDialer(dialer),\r\n grpc.WithTransportCredentials(insecure.NewCredentials()),\r\n )\r\n require.NoError(t, err)\r\n\r\n // Create the service client\r\n client := service.New{serviceName}Client(conn)\r\n\r\n // Return cleanup function\r\n cleanup := func() {\r\n conn.Close()\r\n grpcServer.Stop()\r\n }\r\n\r\n return &testService{\r\n grpcConn: conn,\r\n client: client,\r\n cleanup: cleanup,\r\n }\r\n}\r\n\r\nfunc TestQueryHello(t *testing.T) {\r\n // Set up basic service\r\n svc := setupTestService(t)\r\n defer svc.cleanup()\r\n\r\n tests := []struct {\r\n name string\r\n userName string\r\n wantId string\r\n wantName string\r\n wantErr bool\r\n }{\r\n {\r\n name: "valid hello",\r\n userName: "Alice",\r\n wantId: "1",\r\n wantName: "Alice",\r\n wantErr: false,\r\n },\r\n {\r\n name: "empty name",\r\n userName: "",\r\n wantId: "2",\r\n wantName: "", // Empty name should be preserved\r\n wantErr: false,\r\n },\r\n {\r\n name: "special characters",\r\n userName: "John & Jane",\r\n wantId: "3",\r\n wantName: "John & Jane",\r\n wantErr: false,\r\n },\r\n }\r\n\r\n for _, tt := range tests {\r\n t.Run(tt.name, func(t *testing.T) {\r\n req := &service.QueryHelloRequest{\r\n Name: tt.userName,\r\n }\r\n\r\n resp, err := svc.client.QueryHello(context.Background(), req)\r\n if tt.wantErr {\r\n assert.Error(t, err)\r\n return\r\n }\r\n\r\n assert.NoError(t, err)\r\n assert.NotNil(t, resp.Hello)\r\n assert.Equal(t, tt.wantId, resp.Hello.Id)\r\n assert.Equal(t, tt.wantName, resp.Hello.Name)\r\n })\r\n }\r\n}\r\n\r\nfunc TestSequentialIDs(t *testing.T) {\r\n // Set up basic service\r\n svc := setupTestService(t)\r\n defer svc.cleanup()\r\n\r\n // The first request should get ID "1"\r\n firstReq := &service.QueryHelloRequest{Name: "First"}\r\n firstResp, err := svc.client.QueryHello(context.Background(), firstReq)\r\n require.NoError(t, err)\r\n assert.Equal(t, "1", firstResp.Hello.Id)\r\n\r\n // The second request should get ID "2"\r\n secondReq := &service.QueryHelloRequest{Name: "Second"}\r\n secondResp, err := svc.client.QueryHello(context.Background(), secondReq)\r\n require.NoError(t, err)\r\n assert.Equal(t, "2", secondResp.Hello.Id)\r\n\r\n // The third request should get ID "3"\r\n thirdReq := &service.QueryHelloRequest{Name: "Third"}\r\n thirdResp, err := svc.client.QueryHello(context.Background(), thirdReq)\r\n require.NoError(t, err)\r\n assert.Equal(t, "3", thirdResp.Hello.Id)\r\n}\r\n'; const readmePartialMd = - '## Getting Started\n\nPlugin structure:\n\n ```\n plugins/{originalPluginName}/\n ├── go.mod # Go module file with dependencies\n ├── go.sum # Go checksums file\n ├── src/\n │ ├── main.go # Main plugin implementation\n │ ├── main_test.go # Tests for the plugin\n │ └── schema.graphql # GraphQL schema defining the API\n ├── generated/ # Generated code (created during build)\n └── bin/ # Compiled binaries (created during build)\n └── plugin # The compiled plugin binary\n ```'; + '## Getting Started\r\n\r\nPlugin structure:\r\n\r\n ```\r\n plugins/{originalPluginName}/\r\n ├── go.mod # Go module file with dependencies\r\n ├── go.sum # Go checksums file\r\n ├── src/\r\n │ ├── main.go # Main plugin implementation\r\n │ ├── main_test.go # Tests for the plugin\r\n │ └── schema.graphql # GraphQL schema defining the API\r\n ├── generated/ # Generated code (created during build)\r\n └── bin/ # Compiled binaries (created during build)\r\n └── plugin # The compiled plugin binary\r\n ```'; export default { dockerfile, diff --git a/cli/src/commands/router/commands/plugin/templates/plugin.ts b/cli/src/commands/router/commands/plugin/templates/plugin.ts index 2a54553afa..11722c8f56 100644 --- a/cli/src/commands/router/commands/plugin/templates/plugin.ts +++ b/cli/src/commands/router/commands/plugin/templates/plugin.ts @@ -2,19 +2,19 @@ // This file is auto-generated. Do not edit manually. /* eslint-disable no-template-curly-in-string */ -const gitignore = '# Ignore the binary files\nbin/\n'; +const gitignore = '# Ignore the binary files\r\nbin/\r\n'; const makefile = - '\n.PHONY: build test generate install-wgc\n\ninstall-wgc:\n\t@which wgc > /dev/null 2>&1 || npm install -g wgc@latest\n\nmake: build\n\ntest: install-wgc\n\twgc router plugin test .\n\ngenerate: install-wgc\n\twgc router plugin generate .\n\npublish: generate\n\twgc router plugin publish .\n\nbuild: install-wgc\n\twgc router plugin build . --debug\n'; + '\r\n.PHONY: build test generate install-wgc\r\n\r\ninstall-wgc:\r\n\t@which wgc > /dev/null 2>&1 || npm install -g wgc@latest\r\n\r\nmake: build\r\n\r\ntest: install-wgc\r\n\twgc router plugin test .\r\n\r\ngenerate: install-wgc\r\n\twgc router plugin generate .\r\n\r\npublish: generate\r\n\twgc router plugin publish .\r\n\r\nbuild: install-wgc\r\n\twgc router plugin build . --debug\r\n'; const readmePluginMd = - '# {name} Plugin - Cosmo gRPC Service Example\n\nThis repository contains a simple Cosmo gRPC service plugin that showcases how to design APIs with GraphQL Federation but implement them using gRPC methods instead of traditional resolvers.\n\n## What is this demo about?\n\nThis demo illustrates a key pattern in Cosmo gRPC service development:\n- **Design with GraphQL**: Define your API using GraphQL schema\n- **Implement with gRPC**: Instead of writing GraphQL resolvers, implement gRPC service methods\n- **Bridge the gap**: The Cosmo router connects GraphQL operations to your gRPC implementations\n- **Test-Driven Development**: Test your gRPC service implementation with gRPC client and server without external dependencies\n\nThe plugin demonstrates:\n- How GraphQL types and operations map to gRPC service methods\n- Simple "Hello World" implementation\n- Proper structure for a Cosmo gRPC service plugin\n- How to test your gRPC service implementation with gRPC client and server without external dependencies\n\n{readmeText}\n\n## 🔧 Customizing Your Plugin\n\n- Change the GraphQL schema in `src/schema.graphql` and regenerate the code with `make generate`.\n- Implement the changes in `src/{mainFile}` and test your implementation with `make test`.\n- Build the plugin with `make build`.\n\n## 📚 Learn More\n\nFor more information about Cosmo and building router plugins:\n- [Cosmo Documentation](https://cosmo-docs.wundergraph.com/)\n- [Cosmo Router Plugins Guide](https://cosmo-docs.wundergraph.com/connect/plugins)\n\n---\n\n

Made with ❤️ by WunderGraph

'; + '# {name} Plugin - Cosmo gRPC Service Example\r\n\r\nThis repository contains a simple Cosmo gRPC service plugin that showcases how to design APIs with GraphQL Federation but implement them using gRPC methods instead of traditional resolvers.\r\n\r\n## What is this demo about?\r\n\r\nThis demo illustrates a key pattern in Cosmo gRPC service development:\r\n- **Design with GraphQL**: Define your API using GraphQL schema\r\n- **Implement with gRPC**: Instead of writing GraphQL resolvers, implement gRPC service methods\r\n- **Bridge the gap**: The Cosmo router connects GraphQL operations to your gRPC implementations\r\n- **Test-Driven Development**: Test your gRPC service implementation with gRPC client and server without external dependencies\r\n\r\nThe plugin demonstrates:\r\n- How GraphQL types and operations map to gRPC service methods\r\n- Simple "Hello World" implementation\r\n- Proper structure for a Cosmo gRPC service plugin\r\n- How to test your gRPC service implementation with gRPC client and server without external dependencies\r\n\r\n{readmeText}\r\n\r\n## 🔧 Customizing Your Plugin\r\n\r\n- Change the GraphQL schema in `src/schema.graphql` and regenerate the code with `make generate`.\r\n- Implement the changes in `src/{mainFile}` and test your implementation with `make test`.\r\n- Build the plugin with `make build`.\r\n\r\n## 📚 Learn More\r\n\r\nFor more information about Cosmo and building router plugins:\r\n- [Cosmo Documentation](https://cosmo-docs.wundergraph.com/)\r\n- [Cosmo Router Plugins Guide](https://cosmo-docs.wundergraph.com/connect/plugins)\r\n\r\n---\r\n\r\n

Made with ❤️ by WunderGraph

'; const cursorignore = - '# Ignore the mapping and lock files\ngenerated/mapping.json\ngenerated/service.proto.lock.json\n# Ignore the proto to avoid interpretation issues\ngenerated/service.proto\n# Ignore the plugin binary\nbin/\n'; + '# Ignore the mapping and lock files\r\ngenerated/mapping.json\r\ngenerated/service.proto.lock.json\r\n# Ignore the proto to avoid interpretation issues\r\ngenerated/service.proto\r\n# Ignore the plugin binary\r\nbin/\r\n'; const schemaGraphql = - 'type World {\n """\n The ID of the world\n """\n id: ID!\n """\n The name of the world\n """\n name: String!\n}\n\ntype Query {\n """\n The hello query\n """\n hello(name: String!): World!\n}\n'; + 'type World {\r\n """\r\n The ID of the world\r\n """\r\n id: ID!\r\n """\r\n The name of the world\r\n """\r\n name: String!\r\n}\r\n\r\ntype Query {\r\n """\r\n The hello query\r\n """\r\n hello(name: String!): World!\r\n}\r\n'; export default { gitignore, diff --git a/cli/src/commands/router/commands/plugin/templates/project.ts b/cli/src/commands/router/commands/plugin/templates/project.ts index 2322ae246d..43a7688e85 100644 --- a/cli/src/commands/router/commands/plugin/templates/project.ts +++ b/cli/src/commands/router/commands/plugin/templates/project.ts @@ -2,22 +2,22 @@ // This file is auto-generated. Do not edit manually. /* eslint-disable no-template-curly-in-string */ -const gitignore = '# Ignore the binary files\nrelease/\n'; +const gitignore = '# Ignore the binary files\r\nrelease/\r\n'; const makefile = - '\n.PHONY: install-wgc build download start compose\n\nmake: install-wgc download build compose start\n\ninstall-wgc:\n\t@which wgc > /dev/null 2>&1 || npm install -g wgc@latest\n\nstart:\n\t./release/router\n\ncompose: install-wgc\n\twgc router compose -i graph.yaml -o config.json\n\ndownload: install-wgc\n\t@if [ ! -f release/router ]; then \\\n\t\trm -rf release && wgc router download-binary -o release && chmod +x release/router; \\\n\telse \\\n\t\techo "Router binary already exists, skipping download"; \\\n\tfi\n\nbuild:\n\tcd plugins/{originalPluginName} && make build\n'; + '\r\n.PHONY: install-wgc build download start compose\r\n\r\nmake: install-wgc download build compose start\r\n\r\ninstall-wgc:\r\n\t@which wgc > /dev/null 2>&1 || npm install -g wgc@latest\r\n\r\nstart:\r\n\t./release/router\r\n\r\ncompose: install-wgc\r\n\twgc router compose -i graph.yaml -o config.json\r\n\r\ndownload: install-wgc\r\n\t@if [ ! -f release/router ]; then \\\r\n\t\trm -rf release && wgc router download-binary -o release && chmod +x release/router; \\\r\n\telse \\\r\n\t\techo "Router binary already exists, skipping download"; \\\r\n\tfi\r\n\r\nbuild:\r\n\tcd plugins/{originalPluginName} && make build\r\n'; const readmePluginMd = - '# {name} Plugin - Cosmo Router Example\n\nThis repository contains a simple Cosmo Router plugin that showcases how to design APIs with GraphQL Federation but implement them using gRPC methods instead of traditional resolvers.\n\n## What is this demo about?\n\nThis demo illustrates a key pattern in Cosmo Router plugin development:\n- **Design with GraphQL**: Define your API using GraphQL schema\n- **Implement with gRPC**: Instead of writing GraphQL resolvers, implement gRPC service methods\n- **Bridge the gap**: The Cosmo router connects GraphQL operations to your gRPC implementations\n- **Test-Driven Development**: Test your gRPC service implementation with gRPC client and server without external dependencies\n\nThe plugin demonstrates:\n- How GraphQL types and operations map to gRPC RPC methods\n- Simple "Hello World" implementation\n- Proper structure for a Cosmo Router plugin\n- How to test your gRPC implementation with gRPC client and server without external dependencies\n\n{readmeText}\n\n## 🔧 Customizing Your Plugin\n\n- Change the GraphQL schema in `src/schema.graphql` and regenerate the code with `make generate`.\n- Implement the changes in `src/{mainFile}` and test your implementation with `make test`.\n- Compose your supergraph with `make compose` and restart the router with `make start`.\n\n## 📚 Learn More\n\nFor more information about Cosmo and building router plugins:\n- [Cosmo Documentation](https://cosmo-docs.wundergraph.com/)\n- [Cosmo Router Plugins Guide](https://cosmo-docs.wundergraph.com/connect/plugins)\n\n---\n\n

Made with ❤️ by WunderGraph

'; + '# {name} Plugin - Cosmo Router Example\r\n\r\nThis repository contains a simple Cosmo Router plugin that showcases how to design APIs with GraphQL Federation but implement them using gRPC methods instead of traditional resolvers.\r\n\r\n## What is this demo about?\r\n\r\nThis demo illustrates a key pattern in Cosmo Router plugin development:\r\n- **Design with GraphQL**: Define your API using GraphQL schema\r\n- **Implement with gRPC**: Instead of writing GraphQL resolvers, implement gRPC service methods\r\n- **Bridge the gap**: The Cosmo router connects GraphQL operations to your gRPC implementations\r\n- **Test-Driven Development**: Test your gRPC service implementation with gRPC client and server without external dependencies\r\n\r\nThe plugin demonstrates:\r\n- How GraphQL types and operations map to gRPC RPC methods\r\n- Simple "Hello World" implementation\r\n- Proper structure for a Cosmo Router plugin\r\n- How to test your gRPC implementation with gRPC client and server without external dependencies\r\n\r\n{readmeText}\r\n\r\n## 🔧 Customizing Your Plugin\r\n\r\n- Change the GraphQL schema in `src/schema.graphql` and regenerate the code with `make generate`.\r\n- Implement the changes in `src/{mainFile}` and test your implementation with `make test`.\r\n- Compose your supergraph with `make compose` and restart the router with `make start`.\r\n\r\n## 📚 Learn More\r\n\r\nFor more information about Cosmo and building router plugins:\r\n- [Cosmo Documentation](https://cosmo-docs.wundergraph.com/)\r\n- [Cosmo Router Plugins Guide](https://cosmo-docs.wundergraph.com/connect/plugins)\r\n\r\n---\r\n\r\n

Made with ❤️ by WunderGraph

'; const readmeProjectMd = - '# {name} - Cosmo Router Plugin Project\n\nDesign your API with GraphQL Federation and implement with gRPC using Cosmo Router Plugins\n\n## ✨ Features\n\n- **GraphQL Schema + gRPC Implementation**: Design your API with GraphQL SDL and implement it using gRPC methods\n- **Embedded Subgraphs**: Run subgraphs directly inside the Cosmo Router for improved performance\n- **End-to-End Type Safety**: Auto-generated Go code from your GraphQL schema\n- **Simplified Testing**: Unit test your gRPC implementation with no external dependencies\n\n## 📝 Project Structure\n\nThis project sets up a complete environment for developing and testing Cosmo Router plugins:\n\n```\nproject-root/\n├── plugins/ # Contains all the plugins\n├── graph.yaml # Supergraph configuration\n├── config.json # Composed supergraph (generated)\n├── config.yaml # Router configuration\n├── release/ # Router binary location\n│ └── router # Router binary\n└── Makefile # Automation scripts\n```\n\n## 🚀 Getting Started\n\n### Setup\n\n1. Clone this repository\n2. Run the included Makefile commands\n\n### Available Make Commands\n\nThe Makefile automates the entire workflow with these commands:\n\n- `make`: Runs all commands in sequence (download, build, compose, start)\n- `make download`: Downloads the Cosmo Router binary to the `release` directory\n- `make build`: Builds the plugin from your source code with debug symbols enabled\n- `make generate`: Generates Go code from your GraphQL schema without compilation\n- `make test`: Validates your implementation with integration tests\n- `make compose`: Composes your supergraph from the configuration in `graph.yaml`\n- `make start`: Starts the Cosmo Router with your plugin\n\n### Quick Start\n\nTo get everything running with a single command:\n\n```bash\nmake\n```\n\nThis will:\n1. Download the Cosmo Router binary\n2. Build your plugin from source\n3. Compose your supergraph\n4. Start the router on port 3010\n\n## 🧪 Testing Your Plugin\n\nOnce running, open the GraphQL Playground at [http://localhost:3010](http://localhost:3010) and try this query:\n\n```graphql\nquery {\n hello(name: "World") {\n id\n name\n }\n}\n```\n\n## 🔧 Customizing Your Plugin\n\n1. Modify `src/schema.graphql` to define your GraphQL types and operations\n2. Edit `src/main.go` to implement the corresponding gRPC service methods\n3. Run `make generate` to regenerate code from your updated schema\n4. Run `make build` to compile your plugin\n5. Run `make test` to validate your implementation with integration tests\n6. Run `make compose` to update your supergraph\n7. Run `make start` to restart the router with your changes\n\n## 📚 Learn More\n\nFor more information about Cosmo and building router plugins:\n- [Cosmo Documentation](https://cosmo-docs.wundergraph.com/)\n- [Cosmo Router Plugins Guide](https://cosmo-docs.wundergraph.com/connect/plugins)\n\n---\n\n

Made with ❤️ by WunderGraph

\n'; + '# {name} - Cosmo Router Plugin Project\r\n\r\nDesign your API with GraphQL Federation and implement with gRPC using Cosmo Router Plugins\r\n\r\n## ✨ Features\r\n\r\n- **GraphQL Schema + gRPC Implementation**: Design your API with GraphQL SDL and implement it using gRPC methods\r\n- **Embedded Subgraphs**: Run subgraphs directly inside the Cosmo Router for improved performance\r\n- **End-to-End Type Safety**: Auto-generated Go code from your GraphQL schema\r\n- **Simplified Testing**: Unit test your gRPC implementation with no external dependencies\r\n\r\n## 📝 Project Structure\r\n\r\nThis project sets up a complete environment for developing and testing Cosmo Router plugins:\r\n\r\n```\r\nproject-root/\r\n├── plugins/ # Contains all the plugins\r\n├── graph.yaml # Supergraph configuration\r\n├── config.json # Composed supergraph (generated)\r\n├── config.yaml # Router configuration\r\n├── release/ # Router binary location\r\n│ └── router # Router binary\r\n└── Makefile # Automation scripts\r\n```\r\n\r\n## 🚀 Getting Started\r\n\r\n### Setup\r\n\r\n1. Clone this repository\r\n2. Run the included Makefile commands\r\n\r\n### Available Make Commands\r\n\r\nThe Makefile automates the entire workflow with these commands:\r\n\r\n- `make`: Runs all commands in sequence (download, build, compose, start)\r\n- `make download`: Downloads the Cosmo Router binary to the `release` directory\r\n- `make build`: Builds the plugin from your source code with debug symbols enabled\r\n- `make generate`: Generates Go code from your GraphQL schema without compilation\r\n- `make test`: Validates your implementation with integration tests\r\n- `make compose`: Composes your supergraph from the configuration in `graph.yaml`\r\n- `make start`: Starts the Cosmo Router with your plugin\r\n\r\n### Quick Start\r\n\r\nTo get everything running with a single command:\r\n\r\n```bash\r\nmake\r\n```\r\n\r\nThis will:\r\n1. Download the Cosmo Router binary\r\n2. Build your plugin from source\r\n3. Compose your supergraph\r\n4. Start the router on port 3010\r\n\r\n## 🧪 Testing Your Plugin\r\n\r\nOnce running, open the GraphQL Playground at [http://localhost:3010](http://localhost:3010) and try this query:\r\n\r\n```graphql\r\nquery {\r\n hello(name: "World") {\r\n id\r\n name\r\n }\r\n}\r\n```\r\n\r\n## 🔧 Customizing Your Plugin\r\n\r\n1. Modify `src/schema.graphql` to define your GraphQL types and operations\r\n2. Edit `src/main.go` to implement the corresponding gRPC service methods\r\n3. Run `make generate` to regenerate code from your updated schema\r\n4. Run `make build` to compile your plugin\r\n5. Run `make test` to validate your implementation with integration tests\r\n6. Run `make compose` to update your supergraph\r\n7. Run `make start` to restart the router with your changes\r\n\r\n## 📚 Learn More\r\n\r\nFor more information about Cosmo and building router plugins:\r\n- [Cosmo Documentation](https://cosmo-docs.wundergraph.com/)\r\n- [Cosmo Router Plugins Guide](https://cosmo-docs.wundergraph.com/connect/plugins)\r\n\r\n---\r\n\r\n

Made with ❤️ by WunderGraph

\r\n'; const graphYaml = - 'version: 1\nsubgraphs:\n # Add your other subgraphs here\n - plugin:\n version: 0.0.1\n path: plugins/{originalPluginName}\n'; + 'version: 1\r\nsubgraphs:\r\n # Add your other subgraphs here\r\n - plugin:\r\n version: 0.0.1\r\n path: plugins/{originalPluginName}\r\n'; const routerConfigYaml = - '# yaml-language-server: $schema=https://raw.githubusercontent.com/wundergraph/cosmo/main/router/pkg/config/config.schema.json\n\nversion: "1"\n\nlisten_addr: localhost:3010\n\ndev_mode: true\n\nexecution_config:\n file:\n path: config.json\n\nplugins:\n enabled: true\n path: plugins\n'; + '# yaml-language-server: $schema=https://raw.githubusercontent.com/wundergraph/cosmo/main/router/pkg/config/config.schema.json\r\n\r\nversion: "1"\r\n\r\nlisten_addr: localhost:3010\r\n\r\ndev_mode: true\r\n\r\nexecution_config:\r\n file:\r\n path: config.json\r\n\r\nplugins:\r\n enabled: true\r\n path: plugins\r\n'; export default { gitignore, diff --git a/cli/src/commands/router/commands/plugin/templates/typescript.ts b/cli/src/commands/router/commands/plugin/templates/typescript.ts index 3bd14ca7c5..965f9aef58 100644 --- a/cli/src/commands/router/commands/plugin/templates/typescript.ts +++ b/cli/src/commands/router/commands/plugin/templates/typescript.ts @@ -3,37 +3,37 @@ /* eslint-disable no-template-curly-in-string */ const dockerfile = - 'FROM --platform=$BUILDPLATFORM oven/bun:1.3.0-alpine AS builder\n\n# Multi-platform build arguments\nARG TARGETOS\nARG TARGETARCH\n\nWORKDIR /build\n\n# Copy package files\nCOPY package.json tsconfig.json bun.lock* ./\nCOPY patches/ ./patches/\nCOPY src/ ./src/\nCOPY generated/ ./generated/\n\n# Install dependencies\nRUN bun install\n\nRUN bun x tsc --noEmit\n\n# Set BUN_TARGET based on OS and architecture\nRUN BUN_TARGET="bun-$TARGETOS-$([ "$TARGETARCH" = "amd64" ] && echo "x64" || echo "$TARGETARCH")" && \\\n echo "Building for $BUN_TARGET" && \\\n bun build src/plugin.ts --compile --outfile bin/plugin --target=$BUN_TARGET\n\nFROM --platform=$BUILDPLATFORM scratch\n\nCOPY --from=builder /build/bin/plugin ./{originalPluginName}-plugin\nCOPY --from=builder /build/node_modules/grpc-health-check/proto/health/v1/health.proto /grpc-health-check/proto/health/v1/health.proto\n\nENTRYPOINT ["./{originalPluginName}-plugin"]\n\n'; + 'FROM --platform=$BUILDPLATFORM oven/bun:1.3.0-alpine AS builder\r\n\r\n# Multi-platform build arguments\r\nARG TARGETOS\r\nARG TARGETARCH\r\n\r\nWORKDIR /build\r\n\r\n# Copy package files\r\nCOPY package.json tsconfig.json bun.lock* ./\r\nCOPY patches/ ./patches/\r\nCOPY src/ ./src/\r\nCOPY generated/ ./generated/\r\n\r\n# Install dependencies\r\nRUN bun install\r\n\r\nRUN bun x tsc --noEmit\r\n\r\n# Set BUN_TARGET based on OS and architecture\r\nRUN BUN_TARGET="bun-$TARGETOS-$([ "$TARGETARCH" = "amd64" ] && echo "x64" || echo "$TARGETARCH")" && \\\r\n echo "Building for $BUN_TARGET" && \\\r\n bun build src/plugin.ts --compile --outfile bin/plugin --target=$BUN_TARGET\r\n\r\nFROM --platform=$BUILDPLATFORM scratch\r\n\r\nCOPY --from=builder /build/bin/plugin ./{originalPluginName}-plugin\r\nCOPY --from=builder /build/node_modules/grpc-health-check/proto/health/v1/health.proto /grpc-health-check/proto/health/v1/health.proto\r\n\r\nENTRYPOINT ["./{originalPluginName}-plugin"]\r\n\r\n'; const cursorRules = - "---\ndescription: {name} Plugin Guide\nglobs: src/**\nalwaysApply: false\n---\n\n# {name} Plugin Development Guide\n\nYou are an expert in developing Cosmo Router plugins. You are given a GraphQL schema, and you need to implement the TypeScript code for the plugin.\nYour goal is to implement the plugin in a way that is easy to understand and maintain. You add tests to ensure the plugin works as expected.\n\nAll make commands need to be run from the plugin directory `{pluginDir}`.\n\n## Plugin Structure\n\nA plugin is structured as follows:\n\n```\nplugins/{originalPluginName}/\n├── Makefile # Build automation\n├── package.json # Node.js package definition\n├── tsconfig.json # TypeScript configuration\n├── src/\n│ ├── schema.graphql # GraphQL schema (API contract)\n│ ├── plugin.ts # Plugin implementation\n│ ├── plugin.test.ts # Tests for the plugin\n│ ├── plugin-server.ts # gRPC server setup\n│ └── fs-polyfill.ts # File system polyfill for bundling\n├── generated/ # Auto-generated files (DO NOT EDIT)\n│ ├── service.proto # Generated Protocol Buffers\n│ ├── service_pb.js # Generated JavaScript structures\n│ ├── service_grpc_pb.js # Generated gRPC service\n│ ├── service.proto.lock.json # Generated Protobuf lock file\n│ └── service_pb.d.ts # TypeScript definitions\n└── bin/ # Compiled binaries\n └── plugin # The compiled plugin binary\n```\n\n## Development Workflow\n\n1. When modifying the GraphQL schema in `src/schema.graphql`, you need to regenerate the code with `make generate`.\n2. Look into the generated code in `generated/service.proto`, `generated/service_pb.js`, and `generated/service_grpc_pb.js` to understand the updated API contract and service methods.\n3. Implement the new RPC methods in `src/plugin.ts`.\n4. Add tests to `src/plugin.test.ts` to ensure the plugin works as expected. You need to run `make test` to ensure the tests pass.\n5. Finally, build the plugin with `make build` to ensure the plugin is working as expected.\n6. Your job is done after successfully building the plugin. Don't verify if the binary was created. The build command will take care of that.\n\n**Important**: Never manipulate the files inside `generated` directory yourself. Don't touch the `service.proto`, `service.proto.lock.json`, `service_pb.js`, `service_grpc_pb.js` and TypeScript definition files.\n\nYou can update the TypeScript dependencies by running `bun install` to ensure the dependencies are up to date.\n\n## Implementation Pattern\n\n### Service Integration\n\nIf you need to integrate with other HTTP services, you should use the built-in `fetch` API or a library like `axios`.\nAlways prefer a real integration over mocking. In the tests, you can mock the external service by bootstrapping an HTTP server that returns the expected response.\nIn tests, focus on a well-defined contract and the expected behavior of your service. Structure tests by endpoint, use-cases and use descriptive test names.\n\nHere is an example of how to use the `fetch` API:\n\n```typescript\n// Initialize HTTP client for external API calls\nconst baseURL = \"\";\nconst headers = {\n 'Content-Type': 'application/json',\n // Add other headers as needed\n};\n\n// A HTTP GET request to the external API\nconst getResponse = await fetch(`/`, {\n method: 'GET',\n headers,\n});\nconst getData = await getResponse.json();\n\n// A HTTP POST request to the external API with JSON payload\nconst postResponse = await fetch(`/`, {\n method: 'POST',\n headers,\n body: JSON.stringify(payload),\n});\nconst postData = await postResponse.json();\n\n// Check for success\nif (postResponse.ok) {\n // StatusCode >= 200 && StatusCode < 300\n console.log('Success:', postData);\n}\n```\n\n### gRPC Service Implementation\n\nYour plugin implementation should follow the pattern of implementing the gRPC service interface generated from the GraphQL schema. The service methods receive requests and return responses using the generated protobuf types.\n\n```typescript\nimport { I{serviceName}Server } from '../generated/service_grpc_pb.js';\nimport { QueryRequest, QueryResponse } from '../generated/service_pb.js';\n\nconst {serviceName}Implementation: I{serviceName}Server = {\n queryMethod: (call, callback) => {\n // Access request data\n const input = call.request.getFieldName();\n \n // Create and populate response\n const response = new QueryResponse();\n response.setFieldName(value);\n \n // Send response\n callback(null, response);\n }\n};\n```\n\n\n"; + "---\r\ndescription: {name} Plugin Guide\r\nglobs: src/**\r\nalwaysApply: false\r\n---\r\n\r\n# {name} Plugin Development Guide\r\n\r\nYou are an expert in developing Cosmo Router plugins. You are given a GraphQL schema, and you need to implement the TypeScript code for the plugin.\r\nYour goal is to implement the plugin in a way that is easy to understand and maintain. You add tests to ensure the plugin works as expected.\r\n\r\nAll make commands need to be run from the plugin directory `{pluginDir}`.\r\n\r\n## Plugin Structure\r\n\r\nA plugin is structured as follows:\r\n\r\n```\r\nplugins/{originalPluginName}/\r\n├── Makefile # Build automation\r\n├── package.json # Node.js package definition\r\n├── tsconfig.json # TypeScript configuration\r\n├── src/\r\n│ ├── schema.graphql # GraphQL schema (API contract)\r\n│ ├── plugin.ts # Plugin implementation\r\n│ ├── plugin.test.ts # Tests for the plugin\r\n│ ├── plugin-server.ts # gRPC server setup\r\n│ └── fs-polyfill.ts # File system polyfill for bundling\r\n├── generated/ # Auto-generated files (DO NOT EDIT)\r\n│ ├── service.proto # Generated Protocol Buffers\r\n│ ├── service_pb.js # Generated JavaScript structures\r\n│ ├── service_grpc_pb.js # Generated gRPC service\r\n│ ├── service.proto.lock.json # Generated Protobuf lock file\r\n│ └── service_pb.d.ts # TypeScript definitions\r\n└── bin/ # Compiled binaries\r\n └── plugin # The compiled plugin binary\r\n```\r\n\r\n## Development Workflow\r\n\r\n1. When modifying the GraphQL schema in `src/schema.graphql`, you need to regenerate the code with `make generate`.\r\n2. Look into the generated code in `generated/service.proto`, `generated/service_pb.js`, and `generated/service_grpc_pb.js` to understand the updated API contract and service methods.\r\n3. Implement the new RPC methods in `src/plugin.ts`.\r\n4. Add tests to `src/plugin.test.ts` to ensure the plugin works as expected. You need to run `make test` to ensure the tests pass.\r\n5. Finally, build the plugin with `make build` to ensure the plugin is working as expected.\r\n6. Your job is done after successfully building the plugin. Don't verify if the binary was created. The build command will take care of that.\r\n\r\n**Important**: Never manipulate the files inside `generated` directory yourself. Don't touch the `service.proto`, `service.proto.lock.json`, `service_pb.js`, `service_grpc_pb.js` and TypeScript definition files.\r\n\r\nYou can update the TypeScript dependencies by running `bun install` to ensure the dependencies are up to date.\r\n\r\n## Implementation Pattern\r\n\r\n### Service Integration\r\n\r\nIf you need to integrate with other HTTP services, you should use the built-in `fetch` API or a library like `axios`.\r\nAlways prefer a real integration over mocking. In the tests, you can mock the external service by bootstrapping an HTTP server that returns the expected response.\r\nIn tests, focus on a well-defined contract and the expected behavior of your service. Structure tests by endpoint, use-cases and use descriptive test names.\r\n\r\nHere is an example of how to use the `fetch` API:\r\n\r\n```typescript\r\n// Initialize HTTP client for external API calls\r\nconst baseURL = \"\";\r\nconst headers = {\r\n 'Content-Type': 'application/json',\r\n // Add other headers as needed\r\n};\r\n\r\n// A HTTP GET request to the external API\r\nconst getResponse = await fetch(`/`, {\r\n method: 'GET',\r\n headers,\r\n});\r\nconst getData = await getResponse.json();\r\n\r\n// A HTTP POST request to the external API with JSON payload\r\nconst postResponse = await fetch(`/`, {\r\n method: 'POST',\r\n headers,\r\n body: JSON.stringify(payload),\r\n});\r\nconst postData = await postResponse.json();\r\n\r\n// Check for success\r\nif (postResponse.ok) {\r\n // StatusCode >= 200 && StatusCode < 300\r\n console.log('Success:', postData);\r\n}\r\n```\r\n\r\n### gRPC Service Implementation\r\n\r\nYour plugin implementation should follow the pattern of implementing the gRPC service interface generated from the GraphQL schema. The service methods receive requests and return responses using the generated protobuf types.\r\n\r\n```typescript\r\nimport { I{serviceName}Server } from '../generated/service_grpc_pb.js';\r\nimport { QueryRequest, QueryResponse } from '../generated/service_pb.js';\r\n\r\nconst {serviceName}Implementation: I{serviceName}Server = {\r\n queryMethod: (call, callback) => {\r\n // Access request data\r\n const input = call.request.getFieldName();\r\n \r\n // Create and populate response\r\n const response = new QueryResponse();\r\n response.setFieldName(value);\r\n \r\n // Send response\r\n callback(null, response);\r\n }\r\n};\r\n```\r\n\r\n\r\n"; const debugBuild = - '#!/bin/sh\nDIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)\n\nWG_BUN_DEBUG="true" exec bun --inspect run "$DIR/../src/plugin.ts" "$@" 2>>"$DIR/plugin_stderr.log"'; + '#!/bin/sh\r\nDIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)\r\n\r\nWG_BUN_DEBUG="true" exec bun --inspect run "$DIR/../src/plugin.ts" "$@" 2>>"$DIR/plugin_stderr.log"'; const grpcHealthCheckFilePatch = - "diff --git a/build/src/health.js b/build/src/health.js\nindex 1bfe43a2488ea06e541da92773176d2a822aef1d..7ffad08d970d22bab961ea8950248f391cbd3f50 100644\n--- a/build/src/health.js\n+++ b/build/src/health.js\n@@ -20,13 +20,17 @@ Object.defineProperty(exports, \"__esModule\", { value: true });\n exports.protoPath = exports.HealthImplementation = exports.service = void 0;\n const path = require(\"path\");\n const proto_loader_1 = require(\"@grpc/proto-loader\");\n+\n+const healthProtoPath = (process.env.NODE_ENV === 'test' || process.env.WG_BUN_DEBUG === 'true') ? __dirname : path.dirname(process.execPath);\n+const healthProtoSuffix = (process.env.NODE_ENV === 'test' || process.env.WG_BUN_DEBUG === 'true') ? '../../proto' : `grpc-health-check/proto`;\n+\n const loadedProto = (0, proto_loader_1.loadSync)('health/v1/health.proto', {\n keepCase: true,\n longs: String,\n enums: String,\n defaults: true,\n oneofs: true,\n- includeDirs: [`${__dirname}/../../proto`],\n+ includeDirs: [`${healthProtoPath}/${healthProtoSuffix}`],\n });\n exports.service = loadedProto['grpc.health.v1.Health'];\n const GRPC_STATUS_NOT_FOUND = 5;\n@@ -114,5 +118,5 @@ class HealthImplementation {\n }\n }\n exports.HealthImplementation = HealthImplementation;\n-exports.protoPath = path.resolve(__dirname, '../../proto/health/v1/health.proto');\n+exports.protoPath = path.resolve(healthProtoPath, healthProtoSuffix, 'health/v1/health.proto');\n //# sourceMappingURL=health.js.map\n\\ No newline at end of file\ndiff --git a/src/health.ts b/src/health.ts\nindex b0a8769e7fb2691f9a6abfffaee1ac86858bca8f..5c115536337d8183f9681ce841da9b2a560c4517 100644\n--- a/src/health.ts\n+++ b/src/health.ts\n@@ -24,13 +24,16 @@ import { sendUnaryData, Server, ServerUnaryCall, ServerWritableStream } from './\n import { HealthListRequest } from './generated/grpc/health/v1/HealthListRequest';\n import { HealthListResponse } from './generated/grpc/health/v1/HealthListResponse';\n\n+const healthProtoPath = (process.env.NODE_ENV === 'test' || process.env.WG_BUN_DEBUG === 'true') ? __dirname : path.dirname(process.execPath);\n+const healthProtoSuffix = (process.env.NODE_ENV === 'test' || process.env.WG_BUN_DEBUG === 'true') ? '../../proto' : `grpc-health-check/proto`;\n+\n const loadedProto = loadSync('health/v1/health.proto', {\n keepCase: true,\n longs: String,\n enums: String,\n defaults: true,\n oneofs: true,\n- includeDirs: [`${__dirname}/../../proto`],\n+ includeDirs: [`${healthProtoPath}/${healthProtoSuffix}`],\n });\n\n export const service = loadedProto['grpc.health.v1.Health'] as ServiceDefinition;\n@@ -131,4 +134,4 @@ export class HealthImplementation {\n }\n }\n\n-export const protoPath = path.resolve(__dirname, '../../proto/health/v1/health.proto');\n+export const protoPath = path.resolve(healthProtoPath, healthProtoSuffix, 'health/v1/health.proto');\n"; + "diff --git a/build/src/health.js b/build/src/health.js\r\nindex 1bfe43a2488ea06e541da92773176d2a822aef1d..7ffad08d970d22bab961ea8950248f391cbd3f50 100644\r\n--- a/build/src/health.js\r\n+++ b/build/src/health.js\r\n@@ -20,13 +20,17 @@ Object.defineProperty(exports, \"__esModule\", { value: true });\r\n exports.protoPath = exports.HealthImplementation = exports.service = void 0;\r\n const path = require(\"path\");\r\n const proto_loader_1 = require(\"@grpc/proto-loader\");\r\n+\r\n+const healthProtoPath = (process.env.NODE_ENV === 'test' || process.env.WG_BUN_DEBUG === 'true') ? __dirname : path.dirname(process.execPath);\r\n+const healthProtoSuffix = (process.env.NODE_ENV === 'test' || process.env.WG_BUN_DEBUG === 'true') ? '../../proto' : `grpc-health-check/proto`;\r\n+\r\n const loadedProto = (0, proto_loader_1.loadSync)('health/v1/health.proto', {\r\n keepCase: true,\r\n longs: String,\r\n enums: String,\r\n defaults: true,\r\n oneofs: true,\r\n- includeDirs: [`${__dirname}/../../proto`],\r\n+ includeDirs: [`${healthProtoPath}/${healthProtoSuffix}`],\r\n });\r\n exports.service = loadedProto['grpc.health.v1.Health'];\r\n const GRPC_STATUS_NOT_FOUND = 5;\r\n@@ -114,5 +118,5 @@ class HealthImplementation {\r\n }\r\n }\r\n exports.HealthImplementation = HealthImplementation;\r\n-exports.protoPath = path.resolve(__dirname, '../../proto/health/v1/health.proto');\r\n+exports.protoPath = path.resolve(healthProtoPath, healthProtoSuffix, 'health/v1/health.proto');\r\n //# sourceMappingURL=health.js.map\r\n\\ No newline at end of file\r\ndiff --git a/src/health.ts b/src/health.ts\r\nindex b0a8769e7fb2691f9a6abfffaee1ac86858bca8f..5c115536337d8183f9681ce841da9b2a560c4517 100644\r\n--- a/src/health.ts\r\n+++ b/src/health.ts\r\n@@ -24,13 +24,16 @@ import { sendUnaryData, Server, ServerUnaryCall, ServerWritableStream } from './\r\n import { HealthListRequest } from './generated/grpc/health/v1/HealthListRequest';\r\n import { HealthListResponse } from './generated/grpc/health/v1/HealthListResponse';\r\n\r\n+const healthProtoPath = (process.env.NODE_ENV === 'test' || process.env.WG_BUN_DEBUG === 'true') ? __dirname : path.dirname(process.execPath);\r\n+const healthProtoSuffix = (process.env.NODE_ENV === 'test' || process.env.WG_BUN_DEBUG === 'true') ? '../../proto' : `grpc-health-check/proto`;\r\n+\r\n const loadedProto = loadSync('health/v1/health.proto', {\r\n keepCase: true,\r\n longs: String,\r\n enums: String,\r\n defaults: true,\r\n oneofs: true,\r\n- includeDirs: [`${__dirname}/../../proto`],\r\n+ includeDirs: [`${healthProtoPath}/${healthProtoSuffix}`],\r\n });\r\n\r\n export const service = loadedProto['grpc.health.v1.Health'] as ServiceDefinition;\r\n@@ -131,4 +134,4 @@ export class HealthImplementation {\r\n }\r\n }\r\n\r\n-export const protoPath = path.resolve(__dirname, '../../proto/health/v1/health.proto');\r\n+export const protoPath = path.resolve(healthProtoPath, healthProtoSuffix, 'health/v1/health.proto');\r\n"; const packageJson = - '{\n "name": "plugin-bun",\n "version": "1.0.0",\n "description": "gRPC Plugin using Bun runtime",\n "type": "module",\n "scripts": {\n "build": "bun build src/plugin.ts --compile --outfile bin/plugin",\n "dev": "bun run src/plugin.ts",\n "postinstall": "bun ./node_modules/@protocolbuffers/protoc-gen-js/download-protoc-gen-js.js"\n },\n "dependencies": {\n "@grpc/grpc-js": "^1.14.0",\n "google-protobuf": "^4.0.0",\n "grpc-health-check": "2.1.0"\n },\n "devDependencies": {\n "@protocolbuffers/protoc-gen-js": "4.0.0",\n "@types/bun": "^1.3.1",\n "@types/google-protobuf": "^3.15.12",\n "@types/node": "^20.11.5",\n "grpc-tools": "^1.12.4",\n "grpc_tools_node_protoc_ts": "^5.3.3"\n },\n "patchedDependencies": {\n "grpc-health-check@2.1.0": "patches/grpc-health-check@2.1.0.patch",\n "@protobufjs/inquire@1.1.0": "patches/@protobufjs_inquire@1.1.0.patch"\n }\n}\n'; + '{\r\n "name": "plugin-bun",\r\n "version": "1.0.0",\r\n "description": "gRPC Plugin using Bun runtime",\r\n "type": "module",\r\n "scripts": {\r\n "build": "bun build src/plugin.ts --compile --outfile bin/plugin",\r\n "dev": "bun run src/plugin.ts",\r\n "postinstall": "bun ./node_modules/@protocolbuffers/protoc-gen-js/download-protoc-gen-js.js"\r\n },\r\n "dependencies": {\r\n "@grpc/grpc-js": "^1.14.0",\r\n "google-protobuf": "^4.0.0",\r\n "grpc-health-check": "2.1.0"\r\n },\r\n "devDependencies": {\r\n "@protocolbuffers/protoc-gen-js": "4.0.0",\r\n "@types/bun": "^1.3.1",\r\n "@types/google-protobuf": "^3.15.12",\r\n "@types/node": "^20.11.5",\r\n "grpc-tools": "^1.12.4",\r\n "grpc_tools_node_protoc_ts": "^5.3.3"\r\n },\r\n "patchedDependencies": {\r\n "grpc-health-check@2.1.0": "patches/grpc-health-check@2.1.0.patch",\r\n "@protobufjs/inquire@1.1.0": "patches/@protobufjs_inquire@1.1.0.patch"\r\n }\r\n}\r\n'; const pluginServerTs = - "import * as grpc from '@grpc/grpc-js';\nimport * as os from 'os';\nimport * as path from 'path';\nimport * as fs from 'fs';\nimport { HealthImplementation } from 'grpc-health-check';\n\n/**\n * Plugin server that manages gRPC server with Unix domain socket\n */\nexport class PluginServer {\n private readonly socketPath: string;\n private readonly network: string = 'unix';\n\n private server: grpc.Server;\n private healthImpl: HealthImplementation;\n\n constructor(socketDir: string = os.tmpdir()) {\n // Generate a unique temporary file path\n const tempPath = path.join(socketDir, `plugin_${Date.now()}${Math.floor(Math.random() * 1000000)}`);\n this.socketPath = tempPath;\n\n // Ensure the socket file doesn't exist\n if (fs.existsSync(tempPath)) {\n fs.unlinkSync(tempPath);\n }\n\n // Create the gRPC server\n this.server = new grpc.Server();\n\n // Initialize health check service with overall server status and plugin service\n this.healthImpl = new HealthImplementation();\n this.healthImpl.addToServer(this.server);\n this.healthImpl.setStatus('plugin', 'SERVING');\n }\n\n /**\n * Add a service implementation to the server\n */\n public addService(service: grpc.ServiceDefinition, implementation: grpc.UntypedServiceImplementation): void {\n this.server.addService(service, implementation);\n }\n\n /**\n * Start the server and output handshake information for go-plugin\n */\n public serve(): Promise {\n const address = this.network + \"://\" + this.socketPath;\n\n return new Promise((resolve, reject) => {\n this.server.bindAsync(\n address,\n grpc.ServerCredentials.createInsecure(),\n (error, port) => {\n if (error) {\n reject(error);\n return;\n }\n\n // Output the handshake information for go-plugin\n // Format: VERSION|PROTOCOL_VERSION|NETWORK|ADDRESS|PROTOCOL\n const logEntry = \"1|1|\" +this.network + \"|\" + this.socketPath + \"|grpc\";\n console.log(logEntry);\n\n resolve();\n }\n );\n });\n }\n}\n\n"; + "import * as grpc from '@grpc/grpc-js';\r\nimport * as os from 'os';\r\nimport * as path from 'path';\r\nimport * as fs from 'fs';\r\nimport { HealthImplementation } from 'grpc-health-check';\r\n\r\n/**\r\n * Plugin server that manages gRPC server with Unix domain socket\r\n */\r\nexport class PluginServer {\r\n private readonly socketPath: string;\r\n private readonly network: string = 'unix';\r\n\r\n private server: grpc.Server;\r\n private healthImpl: HealthImplementation;\r\n\r\n constructor(socketDir: string = os.tmpdir()) {\r\n // Generate a unique temporary file path\r\n const tempPath = path.join(socketDir, `plugin_${Date.now()}${Math.floor(Math.random() * 1000000)}`);\r\n this.socketPath = tempPath;\r\n\r\n // Ensure the socket file doesn't exist\r\n if (fs.existsSync(tempPath)) {\r\n fs.unlinkSync(tempPath);\r\n }\r\n\r\n // Create the gRPC server\r\n this.server = new grpc.Server();\r\n\r\n // Initialize health check service with overall server status and plugin service\r\n this.healthImpl = new HealthImplementation();\r\n this.healthImpl.addToServer(this.server);\r\n this.healthImpl.setStatus('plugin', 'SERVING');\r\n }\r\n\r\n /**\r\n * Add a service implementation to the server\r\n */\r\n public addService(service: grpc.ServiceDefinition, implementation: grpc.UntypedServiceImplementation): void {\r\n this.server.addService(service, implementation);\r\n }\r\n\r\n /**\r\n * Start the server and output handshake information for go-plugin\r\n */\r\n public serve(): Promise {\r\n const address = this.network + \"://\" + this.socketPath;\r\n\r\n return new Promise((resolve, reject) => {\r\n this.server.bindAsync(\r\n address,\r\n grpc.ServerCredentials.createInsecure(),\r\n (error, port) => {\r\n if (error) {\r\n reject(error);\r\n return;\r\n }\r\n\r\n // Output the handshake information for go-plugin\r\n // Format: VERSION|PROTOCOL_VERSION|NETWORK|ADDRESS|PROTOCOL\r\n const logEntry = \"1|1|\" +this.network + \"|\" + this.socketPath + \"|grpc\";\r\n console.log(logEntry);\r\n\r\n resolve();\r\n }\r\n );\r\n });\r\n }\r\n}\r\n\r\n"; const pluginTestTs = - 'import { describe, test, expect } from "bun:test";\nimport * as grpc from "@grpc/grpc-js";\nimport type { Subprocess } from "bun";\n\n// Generated gRPC types\nimport { {serviceName}Client } from \'../generated/service_grpc_pb.js\';\nimport { QueryHelloRequest, QueryHelloResponse } from "../generated/service_pb.js";\n\nfunction queryHello(client: {serviceName}Client, name: string): Promise {\n return new Promise((resolve, reject) => {\n const req = new QueryHelloRequest();\n req.setName(name);\n client.queryHello(req, (err, resp) => {\n if (err) {\n reject(err);\n return;\n }\n if (!resp) {\n reject(new Error("empty response"));\n return;\n }\n resolve(resp);\n });\n });\n}\n\ndescribe("{serviceName}Service.queryHello", () => {\n test("returns greeting with sequential world IDs", async () => {\n const [subprocess, address] = await startPluginProcess();\n const client = createClient(address);\n try {\n const cases = [\n { name: "Alice", wantId: "world-1", wantName: "Hello from {serviceName} plugin! Alice" },\n { name: "", wantId: "world-2", wantName: "Hello from {serviceName} plugin! " },\n { name: "John & Jane", wantId: "world-3", wantName: "Hello from {serviceName} plugin! John & Jane" },\n ];\n\n for (const c of cases) {\n const resp = await queryHello(client, c.name);\n const world = resp.getHello();\n expect(world).toBeTruthy();\n expect(world!.getId()).toBe(c.wantId);\n expect(world!.getName()).toBe(c.wantName);\n }\n } finally {\n client.close();\n subprocess.kill();\n }\n });\n\n test("IDs increment across multiple requests in a fresh process", async () => {\n const [subprocess, address] = await startPluginProcess();\n const client = createClient(address);\n try {\n const first = await queryHello(client, "First");\n expect(first.getHello()!.getId()).toBe("world-1");\n\n const second = await queryHello(client, "Second");\n expect(second.getHello()!.getId()).toBe("world-2");\n\n const third = await queryHello(client, "Third");\n expect(third.getHello()!.getId()).toBe("world-3");\n } finally {\n client.close();\n subprocess.kill();\n }\n });\n});\n\n\nasync function startPluginProcess(): Promise<[Subprocess, string]> {\n const proc = Bun.spawn(["bun", "run", "src/plugin.ts"], {\n stdout: "pipe",\n stderr: "inherit",\n });\n\n // Read the first line from stdout and parse the address\n if (!proc.stdout) {\n throw new Error("plugin stdout not available");\n }\n const reader = proc.stdout.getReader();\n const decoder = new TextDecoder();\n const { value } = await reader.read();\n reader.releaseLock();\n\n const text = decoder.decode(value ?? new Uint8Array());\n const firstLine = text.split("\\n")[0]?.trim() ?? "";\n const parts = firstLine.split("|");\n const address = parts[3];\n\n return [proc, address];\n}\n\nfunction createClient(address: string): {serviceName}Client {\n const target = \'unix://\' + address;\n return new {serviceName}Client(target, grpc.credentials.createInsecure());\n}'; + 'import { describe, test, expect } from "bun:test";\r\nimport * as grpc from "@grpc/grpc-js";\r\nimport type { Subprocess } from "bun";\r\n\r\n// Generated gRPC types\r\nimport { {serviceName}Client } from \'../generated/service_grpc_pb.js\';\r\nimport { QueryHelloRequest, QueryHelloResponse } from "../generated/service_pb.js";\r\n\r\nfunction queryHello(client: {serviceName}Client, name: string): Promise {\r\n return new Promise((resolve, reject) => {\r\n const req = new QueryHelloRequest();\r\n req.setName(name);\r\n client.queryHello(req, (err, resp) => {\r\n if (err) {\r\n reject(err);\r\n return;\r\n }\r\n if (!resp) {\r\n reject(new Error("empty response"));\r\n return;\r\n }\r\n resolve(resp);\r\n });\r\n });\r\n}\r\n\r\ndescribe("{serviceName}Service.queryHello", () => {\r\n test("returns greeting with sequential world IDs", async () => {\r\n const [subprocess, address] = await startPluginProcess();\r\n const client = createClient(address);\r\n try {\r\n const cases = [\r\n { name: "Alice", wantId: "world-1", wantName: "Hello from {serviceName} plugin! Alice" },\r\n { name: "", wantId: "world-2", wantName: "Hello from {serviceName} plugin! " },\r\n { name: "John & Jane", wantId: "world-3", wantName: "Hello from {serviceName} plugin! John & Jane" },\r\n ];\r\n\r\n for (const c of cases) {\r\n const resp = await queryHello(client, c.name);\r\n const world = resp.getHello();\r\n expect(world).toBeTruthy();\r\n expect(world!.getId()).toBe(c.wantId);\r\n expect(world!.getName()).toBe(c.wantName);\r\n }\r\n } finally {\r\n client.close();\r\n subprocess.kill();\r\n }\r\n });\r\n\r\n test("IDs increment across multiple requests in a fresh process", async () => {\r\n const [subprocess, address] = await startPluginProcess();\r\n const client = createClient(address);\r\n try {\r\n const first = await queryHello(client, "First");\r\n expect(first.getHello()!.getId()).toBe("world-1");\r\n\r\n const second = await queryHello(client, "Second");\r\n expect(second.getHello()!.getId()).toBe("world-2");\r\n\r\n const third = await queryHello(client, "Third");\r\n expect(third.getHello()!.getId()).toBe("world-3");\r\n } finally {\r\n client.close();\r\n subprocess.kill();\r\n }\r\n });\r\n});\r\n\r\n\r\nasync function startPluginProcess(): Promise<[Subprocess, string]> {\r\n const proc = Bun.spawn(["bun", "run", "src/plugin.ts"], {\r\n stdout: "pipe",\r\n stderr: "inherit",\r\n });\r\n\r\n // Read the first line from stdout and parse the address\r\n if (!proc.stdout) {\r\n throw new Error("plugin stdout not available");\r\n }\r\n const reader = proc.stdout.getReader();\r\n const decoder = new TextDecoder();\r\n const { value } = await reader.read();\r\n reader.releaseLock();\r\n\r\n const text = decoder.decode(value ?? new Uint8Array());\r\n const firstLine = text.split("\\n")[0]?.trim() ?? "";\r\n const parts = firstLine.split("|");\r\n const address = parts[3];\r\n\r\n return [proc, address];\r\n}\r\n\r\nfunction createClient(address: string): {serviceName}Client {\r\n const target = \'unix://\' + address;\r\n return new {serviceName}Client(target, grpc.credentials.createInsecure());\r\n}'; const pluginTs = - "import * as grpc from '@grpc/grpc-js';\nimport { PluginServer } from './plugin-server';\n\n// Import generated gRPC code\nimport { \n {serviceName}Service, \n I{serviceName}Server \n} from '../generated/service_grpc_pb.js';\nimport { \n QueryHelloRequest, \n QueryHelloResponse, \n World \n} from '../generated/service_pb.js';\n\n// Thread-safe counter for generating unique IDs using atomics\nconst counterBuffer = new SharedArrayBuffer(4);\nconst counterArray = new Int32Array(counterBuffer);\nAtomics.store(counterArray, 0, 0); // Initialize counter to 0\n\n// Define the service implementation using the generated types\nconst {serviceName}Implementation: I{serviceName}Server = {\n queryHello: (call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => {\n const name = call.request.getName();\n\n const currentCounter = Atomics.add(counterArray, 0, 1) + 1;\n\n const world = new World();\n world.setId(`world-`+currentCounter);\n world.setName(`Hello from {serviceName} plugin! `+ name);\n\n const response = new QueryHelloResponse();\n response.setHello(world);\n\n callback(null, response);\n }\n};\n\nfunction run() {\n // Create the plugin server (health check automatically initialized)\n const pluginServer = new PluginServer();\n \n // Add the {serviceName} service\n pluginServer.addService({serviceName}Service, {serviceName}Implementation);\n\n // Start the server\n pluginServer.serve().catch((error) => {\n console.error('Failed to start plugin server:', error);\n process.exit(1);\n });\n}\n\nrun();\n"; + "import * as grpc from '@grpc/grpc-js';\r\nimport { PluginServer } from './plugin-server';\r\n\r\n// Import generated gRPC code\r\nimport { \r\n {serviceName}Service, \r\n I{serviceName}Server \r\n} from '../generated/service_grpc_pb.js';\r\nimport { \r\n QueryHelloRequest, \r\n QueryHelloResponse, \r\n World \r\n} from '../generated/service_pb.js';\r\n\r\n// Thread-safe counter for generating unique IDs using atomics\r\nconst counterBuffer = new SharedArrayBuffer(4);\r\nconst counterArray = new Int32Array(counterBuffer);\r\nAtomics.store(counterArray, 0, 0); // Initialize counter to 0\r\n\r\n// Define the service implementation using the generated types\r\nconst {serviceName}Implementation: I{serviceName}Server = {\r\n queryHello: (call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => {\r\n const name = call.request.getName();\r\n\r\n const currentCounter = Atomics.add(counterArray, 0, 1) + 1;\r\n\r\n const world = new World();\r\n world.setId(`world-`+currentCounter);\r\n world.setName(`Hello from {serviceName} plugin! `+ name);\r\n\r\n const response = new QueryHelloResponse();\r\n response.setHello(world);\r\n\r\n callback(null, response);\r\n }\r\n};\r\n\r\nfunction run() {\r\n // Create the plugin server (health check automatically initialized)\r\n const pluginServer = new PluginServer();\r\n \r\n // Add the {serviceName} service\r\n pluginServer.addService({serviceName}Service, {serviceName}Implementation);\r\n\r\n // Start the server\r\n pluginServer.serve().catch((error) => {\r\n console.error('Failed to start plugin server:', error);\r\n process.exit(1);\r\n });\r\n}\r\n\r\nrun();\r\n"; const protobufjsInquirePatch = - 'diff --git a/index.js b/index.js\nindex 33778b5539b7fcd7a1e99474a4ecb1745fdfe508..c1520ca11267fc4726ea8b10fe89c8386a2d6e8f 100644\n--- a/index.js\n+++ b/index.js\n@@ -1,6 +1,10 @@\n "use strict";\n module.exports = inquire;\n\n+// Note: This code is already present in the repository here:\n+// https://github.com/protobufjs/protobuf.js/blob/master/lib/inquire/index.js\n+// However the problem is that the build process is not working so it has not gotten released\n+\n /**\n * Requires a module only if available.\n * @memberof util\n@@ -9,9 +13,29 @@ module.exports = inquire;\n */\n function inquire(moduleName) {\n try {\n- var mod = eval("quire".replace(/^/,"re"))(moduleName); // eslint-disable-line no-eval\n- if (mod && (mod.length || Object.keys(mod).length))\n- return mod;\n- } catch (e) {} // eslint-disable-line no-empty\n- return null;\n+ if (typeof require !== "function") {\n+ return null;\n+ }\n+ var mod = require(moduleName);\n+ if (mod && (mod.length || Object.keys(mod).length)) return mod;\n+ return null;\n+ } catch (err) {\n+ // ignore\n+ return null;\n+ }\n }\n+\n+/*\n+// maybe worth a shot to prevent renaming issues:\n+// see: https://github.com/webpack/webpack/blob/master/lib/dependencies/CommonJsRequireDependencyParserPlugin.js\n+// triggers on:\n+// - expression require.cache\n+// - expression require (???)\n+// - call require\n+// - call require:commonjs:item\n+// - call require:commonjs:context\n+\n+Object.defineProperty(Function.prototype, "__self", { get: function() { return this; } });\n+var r = require.__self;\n+delete Function.prototype.__self;\n+*/\n\\ No newline at end of file\n'; + 'diff --git a/index.js b/index.js\r\nindex 33778b5539b7fcd7a1e99474a4ecb1745fdfe508..c1520ca11267fc4726ea8b10fe89c8386a2d6e8f 100644\r\n--- a/index.js\r\n+++ b/index.js\r\n@@ -1,6 +1,10 @@\r\n "use strict";\r\n module.exports = inquire;\r\n\r\n+// Note: This code is already present in the repository here:\r\n+// https://github.com/protobufjs/protobuf.js/blob/master/lib/inquire/index.js\r\n+// However the problem is that the build process is not working so it has not gotten released\r\n+\r\n /**\r\n * Requires a module only if available.\r\n * @memberof util\r\n@@ -9,9 +13,29 @@ module.exports = inquire;\r\n */\r\n function inquire(moduleName) {\r\n try {\r\n- var mod = eval("quire".replace(/^/,"re"))(moduleName); // eslint-disable-line no-eval\r\n- if (mod && (mod.length || Object.keys(mod).length))\r\n- return mod;\r\n- } catch (e) {} // eslint-disable-line no-empty\r\n- return null;\r\n+ if (typeof require !== "function") {\r\n+ return null;\r\n+ }\r\n+ var mod = require(moduleName);\r\n+ if (mod && (mod.length || Object.keys(mod).length)) return mod;\r\n+ return null;\r\n+ } catch (err) {\r\n+ // ignore\r\n+ return null;\r\n+ }\r\n }\r\n+\r\n+/*\r\n+// maybe worth a shot to prevent renaming issues:\r\n+// see: https://github.com/webpack/webpack/blob/master/lib/dependencies/CommonJsRequireDependencyParserPlugin.js\r\n+// triggers on:\r\n+// - expression require.cache\r\n+// - expression require (???)\r\n+// - call require\r\n+// - call require:commonjs:item\r\n+// - call require:commonjs:context\r\n+\r\n+Object.defineProperty(Function.prototype, "__self", { get: function() { return this; } });\r\n+var r = require.__self;\r\n+delete Function.prototype.__self;\r\n+*/\r\n\\ No newline at end of file\r\n'; const readmePartialMd = - '## Getting Started\n\nPlugin structure:\n\n ```\n plugins/{originalPluginName}/\n ├── package.json # Package.json file with dependencies\n ├── src/\n │ ├── plugin.ts # Main plugin implementation\n │ ├── plugin.test.ts # Main plugin implementation tests\n │ ├── fs-polyfill.ts # Polyfill to help bundling into a binary\n │ ├── plugin-server.ts # Used to initialize the plugin as a server\n │ └── schema.graphql # GraphQL schema defining the API\n ├── generated/ # Generated code (created during build)\n └── bin/ # Compiled binaries (created during build)\n └── plugin # The compiled plugin binary\n ```'; + '## Getting Started\r\n\r\nPlugin structure:\r\n\r\n ```\r\n plugins/{originalPluginName}/\r\n ├── package.json # Package.json file with dependencies\r\n ├── src/\r\n │ ├── plugin.ts # Main plugin implementation\r\n │ ├── plugin.test.ts # Main plugin implementation tests\r\n │ ├── fs-polyfill.ts # Polyfill to help bundling into a binary\r\n │ ├── plugin-server.ts # Used to initialize the plugin as a server\r\n │ └── schema.graphql # GraphQL schema defining the API\r\n ├── generated/ # Generated code (created during build)\r\n └── bin/ # Compiled binaries (created during build)\r\n └── plugin # The compiled plugin binary\r\n ```'; const tsconfig = - '{\n "compilerOptions": {\n "strict": true,\n "target": "ES2020",\n "module": "ESNext",\n "moduleResolution": "Bundler",\n "esModuleInterop": true,\n "skipLibCheck": true,\n "types": ["bun"]\n },\n "include": ["src/**/*.ts"]\n}\n\n'; + '{\r\n "compilerOptions": {\r\n "strict": true,\r\n "target": "ES2020",\r\n "module": "ESNext",\r\n "moduleResolution": "Bundler",\r\n "esModuleInterop": true,\r\n "skipLibCheck": true,\r\n "types": ["bun"]\r\n },\r\n "include": ["src/**/*.ts"]\r\n}\r\n\r\n'; export default { dockerfile, diff --git a/connect-go/gen/proto/wg/cosmo/platform/v1/platform.pb.go b/connect-go/gen/proto/wg/cosmo/platform/v1/platform.pb.go index 68057dd266..16cd88430d 100644 --- a/connect-go/gen/proto/wg/cosmo/platform/v1/platform.pb.go +++ b/connect-go/gen/proto/wg/cosmo/platform/v1/platform.pb.go @@ -14189,7 +14189,7 @@ func (x *GetOrganizationBySlugResponse) GetOrganization() *Organization { return nil } -// * +//* // Billing type GetBillingPlansRequest struct { state protoimpl.MessageState @@ -14581,7 +14581,7 @@ func (x *UpgradePlanResponse) GetResponse() *Response { return nil } -// * +//* // MetricsDashboard type GetGraphMetricsRequest struct { state protoimpl.MessageState diff --git a/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts b/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts index 72e8b9054e..396ea04033 100644 --- a/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts +++ b/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts @@ -2449,7 +2449,7 @@ export const configureSubgraphCheckExtensions = { } as const; /** - * + * * Billing * ----------------------------------------------------------------------------------------------------------------------------- * Return the available billing plans diff --git a/connect/src/wg/cosmo/platform/v1/platform_connect.ts b/connect/src/wg/cosmo/platform/v1/platform_connect.ts index b42f5d0cc6..1e6d014f8f 100644 --- a/connect/src/wg/cosmo/platform/v1/platform_connect.ts +++ b/connect/src/wg/cosmo/platform/v1/platform_connect.ts @@ -1680,7 +1680,7 @@ export const PlatformService = { kind: MethodKind.Unary, }, /** - * + * * Billing * ----------------------------------------------------------------------------------------------------------------------------- * Return the available billing plans diff --git a/docs/SETUP-WSL.md b/docs/SETUP-WSL.md new file mode 100644 index 0000000000..6630928472 --- /dev/null +++ b/docs/SETUP-WSL.md @@ -0,0 +1,120 @@ +# Cosmo setup on WSL + +This guide follows the standard Cosmo setup steps with **WSL-specific notes**. Run all commands from your WSL terminal (e.g. Ubuntu). + +## 1. Clone the repo + +```bash +git clone https://github.com/wundergraph/cosmo.git +cd cosmo +``` + +## 2. Prerequisites (in WSL) + +Install and verify: + +| Tool | Version | Install / check | +|------|---------|-----------------| +| **Go** | ≥ 1.25 | `sudo apt install golang-go` or [go.dev/dl](https://go.dev/dl/). Ensure `$HOME/go/bin` is on PATH: `export PATH="$PATH:$(go env GOPATH)/bin"` (add to `~/.bashrc` or `~/.zshrc`) | +| **Node.js** | ≥ 22.11.0 | `nvm install 22 && nvm use 22` (install [nvm](https://github.com/nvm-sh/nvm) first if needed) | +| **pnpm** | 9 | `npm install -g pnpm@9` | +| **Docker** | Engine + Compose V2 | Use **Docker Desktop for Windows** with WSL 2 backend, or [Docker Engine inside WSL](https://docs.docker.com/desktop/wsl/). From WSL, run `docker -v` and `docker compose version` | + +Quick check: + +```bash +go version +node -v # expect v22.x +pnpm -v # expect 9.x +docker -v +docker compose version +``` + +## 3. Copy environment files + +```bash +cp controlplane/.env.example controlplane/.env +cp studio/.env.local.example studio/.env.local +cp cli/.env.example cli/.env +``` + +Edit the files if you need to change defaults (e.g. ports or URLs). + +## 4. Docker: enable host networking + +**Why:** Keycloak rejects non-HTTPS traffic from non-localhost. Host networking makes requests appear as localhost inside the container. + +- **Docker Desktop:** Settings → Resources → Network → enable **“Enable host networking”**. +- **Docker Engine in WSL:** If you use `dockerd` directly, ensure host network mode is available and that Keycloak is reached as localhost (e.g. via `network_mode: host` in compose if you use it). + +Restart Docker after changing settings. + +## 5. Bootstrap the repo + +From the repo root **in WSL** (do not run `make` from Git Bash, PowerShell, or CMD—Unix scripts in the repo will fail): + +```bash +make +``` + +This installs dependencies, generates code, starts infra containers (Postgres, Keycloak, etc.), and builds libraries. It can take several minutes. + +## 6. Wait for Keycloak + +Open [http://localhost:8080](http://localhost:8080/) in your browser. Wait until you see the Keycloak sign-in page. + +If 8080 isn’t reachable from Windows, use WSL’s IP or `localhost` from inside WSL (`curl http://localhost:8080`). + +## 7. Migrations and seed + +```bash +make migrate && make seed +``` + +## 8. Start the control plane (Terminal 1) + +**Studio needs the control plane to be running** so it can call `/v1/auth/session` (port 3001). If you skip this or stop it, Studio will show connection errors and auth will fail. + +```bash +make start-cp +``` + +Leave this running. + +## 9. Start Studio (Terminal 2) + +Open a second WSL terminal, `cd` to the repo root, then: + +```bash +make start-studio +``` + +## 10. Log in to Studio + +Open [http://localhost:3000](http://localhost:3000/) and sign in with: + +- **Username:** `foo@wundergraph.com` +- **Password:** `wunder@123` + +## 11. Verify + +Confirm Studio loads and that UI updates when you change components under `studio/`. + +--- + +## WSL tips + +- **Store the repo on the WSL filesystem** (e.g. `~/cosmo` or `/home//cosmo`) for best performance with Node/pnpm/Docker. +- **Ports:** If `localhost` from Windows doesn’t reach a service, try `http://:3000` or use the same browser from inside WSL (e.g. WSLg). +- **Cleanup:** To tear down infra and volumes: `make infra-down-v`. Then you can run `make` again to bootstrap from scratch. + +For full local dev (demos, subgraphs, router), see [CONTRIBUTING.md](./CONTRIBUTING.md#local-development). + +### Seed hangs with no output + +`make seed` connects to Keycloak first; if you see no output, it is usually waiting on Keycloak or the database. + +1. **Confirm infra is up:** `docker compose -f docker-compose.yml --profile dev ps` — postgres and keycloak should be "Up". +2. **Confirm Keycloak is ready:** In a browser or from WSL run `curl -s -o /dev/null -w "%{http_code}" http://localhost:8080` — you want `200` or `302`. If it fails or hangs, Keycloak is not ready or not reachable; wait a minute after `make` and try again. +3. **Confirm controlplane env:** In `controlplane/.env`, `KC_API_URL` should be `http://localhost:8080` and `DB_URL` should point at `localhost:5432` when running from WSL. +4. After the change in seed, you should see `Seed: connecting to Keycloak and database...` as soon as the script starts; if that appears and then it hangs, the hang is at Keycloak or Postgres.