Skip to content

Commit a4bf34f

Browse files
authored
Generate grpc-js services and clients (RooCodeInc#4199)
* Generate clientImpls and services for grpc-js. Generate grpc-js services and clients (as opposed to the generic service definition) The grpc-js clients are needed to connet to external gRPC services, ie the host bridge. Switch the standalone gRPC service to use the grpc-js service defintions, these have the correct serialize/deserialize methods and fix the camel/snake case issue. * Formatting
1 parent 227c719 commit a4bf34f

File tree

5 files changed

+53
-99
lines changed

5 files changed

+53
-99
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ coverage
2222
*evals.env
2323

2424
# Generated proto files
25+
src/generated/
2526
src/core/controller/*/methods.ts
2627
src/core/controller/*/index.ts
2728
src/core/controller/grpc-service-config.ts

proto/build-proto.js

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,24 @@ import os from "os"
1010

1111
import { createRequire } from "module"
1212
const require = createRequire(import.meta.url)
13-
const protoc = path.join(require.resolve("grpc-tools"), "../bin/protoc")
13+
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
1414

15-
const __filename = fileURLToPath(import.meta.url)
16-
const SCRIPT_DIR = path.dirname(__filename)
15+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
1716
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
17+
1818
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
19+
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-js")
20+
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone", "proto")
1921

2022
const isWindows = process.platform === "win32"
21-
const tsProtoPlugin = isWindows
23+
const TS_PROTO_PLUGIN = isWindows
2224
? path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
2325
: require.resolve("ts-proto/protoc-gen-ts_proto")
2426

2527
const TS_PROTO_OPTIONS = [
2628
"env=node",
2729
"esModuleInterop=true",
28-
2930
"outputIndex=true", // output an index file for each package which exports all protos in the package.
30-
"outputServices=generic-definitions",
31-
3231
"useOptionals=messages", // Message fields are optional, scalars are not.
3332
"useDate=false", // Timestamp fields will not be automatically converted to Date.
3433
]
@@ -70,7 +69,9 @@ async function main() {
7069
checkAppleSiliconCompatibility()
7170

7271
// Create output directories if they don't exist
73-
await fs.mkdir(TS_OUT_DIR, { recursive: true })
72+
for (const dir of [TS_OUT_DIR, GRPC_JS_OUT_DIR, DESCRIPTOR_OUT_DIR]) {
73+
await fs.mkdir(dir, { recursive: true })
74+
}
7475

7576
await cleanup()
7677

@@ -81,28 +82,12 @@ async function main() {
8182
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR, realpath: true })
8283
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), SCRIPT_DIR)
8384

84-
// Build the protoc command with proper path handling for cross-platform
85-
const tsProtocCommand = [
86-
protoc,
87-
`--proto_path="${SCRIPT_DIR}"`,
88-
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
89-
`--ts_proto_out="${TS_OUT_DIR}"`,
90-
`--ts_proto_opt=${TS_PROTO_OPTIONS.join(",")} `,
91-
...protoFiles,
92-
].join(" ")
93-
try {
94-
log_verbose(chalk.cyan(`Generating TypeScript code for:\n${protoFiles.join("\n")}...`))
95-
execSync(tsProtocCommand, { stdio: "inherit" })
96-
} catch (error) {
97-
console.error(chalk.red("Error generating TypeScript for proto files:"), error)
98-
process.exit(1)
99-
}
85+
tsProtoc(TS_OUT_DIR, protoFiles, ["outputServices=generic-definitions", ...TS_PROTO_OPTIONS])
86+
tsProtoc(GRPC_JS_OUT_DIR, protoFiles, ["outputServices=grpc-js", ...TS_PROTO_OPTIONS])
10087

101-
const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
102-
await fs.mkdir(descriptorOutDir, { recursive: true })
103-
const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb")
88+
const descriptorFile = path.join(DESCRIPTOR_OUT_DIR, "descriptor_set.pb")
10489
const descriptorProtocCommand = [
105-
protoc,
90+
PROTOC,
10691
`--proto_path="${SCRIPT_DIR}"`,
10792
`--descriptor_set_out="${descriptorFile}"`,
10893
"--include_imports",
@@ -129,6 +114,25 @@ async function main() {
129114
console.log(chalk.bold.blue("Finished Protocol Buffer code generation."))
130115
}
131116

117+
async function tsProtoc(outDir, protoFiles, protoOptions) {
118+
// Build the protoc command with proper path handling for cross-platform
119+
const tsProtocCommand = [
120+
PROTOC,
121+
`--proto_path="${SCRIPT_DIR}"`,
122+
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
123+
`--ts_proto_out="${outDir}"`,
124+
`--ts_proto_opt=${protoOptions.join(",")} `,
125+
...protoFiles,
126+
].join(" ")
127+
try {
128+
log_verbose(chalk.cyan(`Generating TypeScript code in ${outDir} for:\n${protoFiles.join("\n")}...`))
129+
execSync(tsProtocCommand, { stdio: "inherit" })
130+
} catch (error) {
131+
console.error(chalk.red("Error generating TypeScript for proto files:"), error)
132+
process.exit(1)
133+
}
134+
}
135+
132136
/**
133137
* Generate a gRPC client configuration file for the webview
134138
* This eliminates the need for manual imports and client creation in grpc-client.ts
@@ -141,7 +145,7 @@ async function generateGrpcClientConfig() {
141145
const serviceExports = []
142146

143147
// Process each service in the serviceNameMap
144-
for (const [dirName, fullServiceName] of Object.entries(serviceNameMap)) {
148+
for (const [dirName, _fullServiceName] of Object.entries(serviceNameMap)) {
145149
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
146150

147151
// Add import statement

scripts/generate-server-setup.mjs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ function generateHandlersAndExports() {
2929
const dir = domain.charAt(0).toLowerCase() + domain.slice(1)
3030
imports.push(`// ${domain} Service`)
3131
handlerSetup.push(` // ${domain} Service`)
32-
handlerSetup.push(` server.addService(proto.cline.${name}.service, {`)
32+
handlerSetup.push(` server.addService(cline.${name}Service, {`)
3333
for (const [rpcName, rpc] of Object.entries(def.service)) {
3434
imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`)
35-
const requestType = "proto.cline." + rpc.requestType.type.name
35+
const requestType = "cline." + rpc.requestType.type.name
3636
if (rpc.requestStream) {
3737
throw new Error("Request streaming is not supported")
3838
}
3939
if (rpc.responseStream) {
4040
handlerSetup.push(` ${rpcName}: wrapStreamingResponse<${requestType},void>(${rpcName}, controller),`)
4141
} else {
42-
const responseType = "proto.cline." + rpc.responseType.type.name
42+
const responseType = "cline." + rpc.responseType.type.name
4343
handlerSetup.push(` ${rpcName}: wrapper<${requestType},${responseType}>(${rpcName}, controller),`)
4444
}
4545
}
@@ -60,14 +60,13 @@ const scriptName = path.basename(fileURLToPath(import.meta.url))
6060
let output = `// GENERATED CODE -- DO NOT EDIT!
6161
// Generated by ${scriptName}
6262
import * as grpc from "@grpc/grpc-js"
63-
import * as proto from "@/shared/proto"
63+
import { cline } from "../generated/grpc-js"
6464
import { Controller } from "../core/controller"
6565
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
6666
6767
${imports}
6868
export function addServices(
6969
server: grpc.Server,
70-
proto: any,
7170
controller: Controller,
7271
wrapper: GrpcHandlerWrapper,
7372
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,

src/standalone/standalone.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as health from "grpc-health-check"
55
import { activate } from "../extension"
66
import { Controller } from "../core/controller"
77
import { extensionContext, outputChannel, postMessage } from "./vscode-context"
8-
import { packageDefinition, proto, log, camelToSnakeCase, snakeToCamelCase } from "./utils"
8+
import { getPackageDefinition, log } from "./utils"
99
import { GrpcHandler, GrpcStreamingResponseHandler } from "./grpc-types"
1010
import { addServices } from "./server-setup"
1111
import { StreamingResponseHandler } from "@/core/controller/grpc-handler"
@@ -22,10 +22,10 @@ function main() {
2222
healthImpl.addToServer(server)
2323

2424
// Add all the handlers for the ProtoBus services to the server.
25-
addServices(server, proto, controller, wrapHandler, wrapStreamingResponseHandler)
25+
addServices(server, controller, wrapHandler, wrapStreamingResponseHandler)
2626

2727
// Set up reflection.
28-
const reflection = new ReflectionService(packageDefinition)
28+
const reflection = new ReflectionService(getPackageDefinition())
2929
reflection.addToServer(server)
3030

3131
// Start the server.
@@ -58,10 +58,8 @@ function wrapHandler<TRequest, TResponse>(
5858
return async (call: grpc.ServerUnaryCall<TRequest, TResponse>, callback: grpc.sendUnaryData<TResponse>) => {
5959
try {
6060
log(`gRPC request: ${call.getPath()}`)
61-
const result = await handler(controller, snakeToCamelCase(call.request))
62-
// The grpc-js serializer expects the proto message to be in the same
63-
// case as the proto file. This is a work around until we find a solution.
64-
callback(null, camelToSnakeCase(result))
61+
const result = await handler(controller, call.request)
62+
callback(null, result)
6563
} catch (err: any) {
6664
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
6765
callback({
@@ -83,9 +81,7 @@ function wrapStreamingResponseHandler<TRequest, TResponse>(
8381

8482
const responseHandler: StreamingResponseHandler = (response, isLast, sequenceNumber) => {
8583
try {
86-
// The grpc-js serializer expects the proto message to be in the same
87-
// case as the proto file. This is a work around until we find a solution.
88-
call.write(camelToSnakeCase(response)) // Use a bound version of call.write to maintain proper 'this' context
84+
call.write(response) // Use a bound version of call.write to maintain proper 'this' context
8985

9086
if (isLast === true) {
9187
log(`Closing stream for ${requestId}`)
@@ -96,7 +92,7 @@ function wrapStreamingResponseHandler<TRequest, TResponse>(
9692
return Promise.reject(error)
9793
}
9894
}
99-
await handler(controller, snakeToCamelCase(call.request), responseHandler, requestId)
95+
await handler(controller, call.request, responseHandler, requestId)
10096
} catch (err: any) {
10197
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
10298
call.destroy({

src/standalone/utils.ts

Lines changed: 8 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,58 +8,12 @@ const log = (...args: unknown[]) => {
88
console.log(`[${timestamp}]`, "#bot.cline.server.ts", ...args)
99
}
1010

11-
// Load service definitions.
12-
const descriptorSet = fs.readFileSync("proto/descriptor_set.pb")
13-
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(descriptorSet)
14-
const healthDef = protoLoader.loadSync(health.protoPath)
15-
const packageDefinition = { ...clineDef, ...healthDef }
16-
const proto = grpc.loadPackageDefinition(packageDefinition) as unknown
17-
18-
// Helper function to convert camelCase to snake_case
19-
function camelToSnakeCase(obj: any): any {
20-
if (obj === null || typeof obj !== "object") {
21-
return obj
22-
}
23-
24-
if (Array.isArray(obj)) {
25-
return obj.map(camelToSnakeCase)
26-
}
27-
28-
return Object.keys(obj).reduce((acc: any, key: string) => {
29-
// Convert key from camelCase to snake_case
30-
const snakeKey = key
31-
.replace(/([A-Z])/g, "_$1")
32-
.replace(/^_+/, "")
33-
.toLowerCase()
34-
35-
// Convert value recursively if it's an object
36-
const value = obj[key]
37-
acc[snakeKey] = camelToSnakeCase(value)
38-
39-
return acc
40-
}, {})
41-
}
42-
43-
// Helper function to convert snake_case to camelCase
44-
function snakeToCamelCase(obj: any): any {
45-
if (obj === null || typeof obj !== "object") {
46-
return obj
47-
}
48-
49-
if (Array.isArray(obj)) {
50-
return obj.map(snakeToCamelCase)
51-
}
52-
53-
return Object.keys(obj).reduce((acc: any, key: string) => {
54-
// Convert key from snake_case to camelCase
55-
const camelKey = key.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase())
56-
57-
// Convert value recursively if it's an object
58-
const value = obj[key]
59-
acc[camelKey] = snakeToCamelCase(value)
60-
61-
return acc
62-
}, {})
11+
function getPackageDefinition() {
12+
// Load service definitions.
13+
const descriptorSet = fs.readFileSync("proto/descriptor_set.pb")
14+
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(descriptorSet)
15+
const healthDef = protoLoader.loadSync(health.protoPath)
16+
const packageDefinition = { ...clineDef, ...healthDef }
17+
return packageDefinition
6318
}
64-
65-
export { packageDefinition, proto, log, camelToSnakeCase, snakeToCamelCase }
19+
export { getPackageDefinition, log }

0 commit comments

Comments
 (0)