From b7096a77dbfe6b0468362caedd5cbb06d7301346 Mon Sep 17 00:00:00 2001 From: Sarah Fortune Date: Wed, 4 Jun 2025 17:42:19 -0700 Subject: [PATCH] Add type checking for protobus RPC handlers. (#4020) * Add type checking for protobus RPC handlers. Add type parameters to the wrapper function when generating the server-setup file. So that the parameters of the RPC handler will be type checked against types defined in the service. The generated code looks like this: ``` server.addService(proto.cline.CheckpointsService.service, { checkpointDiff: wrapper(checkpointDiff, controller), checkpointRestore: wrapper(checkpointRestore, controller), }); ``` Add an index.ts file to the proto directory so all the proto types can be imported without having to know which proto message is defined in which file (the proto descriptor set doesn't have this information). * Generate index.ts with protoc instead of doing it ourselves. Turn on the option 'dontExportCommonSymbols' in protoc, so that each generated proto file is not trying to export the same utility functions. Generate all the protos with the same protoc command. * cleanup --- proto/build-proto.js | 99 +++++++------------ scripts/generate-server-setup.mjs | 8 +- .../ui/subscribeToAccountButtonClicked.ts | 2 +- 3 files changed, 41 insertions(+), 68 deletions(-) diff --git a/proto/build-proto.js b/proto/build-proto.js index 7e309a5e08..0e51d1eeb3 100755 --- a/proto/build-proto.js +++ b/proto/build-proto.js @@ -12,38 +12,6 @@ import { createRequire } from "module" const require = createRequire(import.meta.url) const protoc = path.join(require.resolve("grpc-tools"), "../bin/protoc") -// Check for Apple Silicon compatibility -function checkAppleSiliconCompatibility() { - // Only run check on macOS - if (process.platform !== "darwin") { - return - } - - // Check if running on Apple Silicon - const cpuArchitecture = os.arch() - if (cpuArchitecture === "arm64") { - try { - // Check if Rosetta is installed - const rosettaCheck = execSync('/usr/bin/pgrep oahd || echo "NOT_INSTALLED"').toString().trim() - - if (rosettaCheck === "NOT_INSTALLED") { - console.log(chalk.yellow("Detected Apple Silicon (ARM64) architecture.")) - console.log( - chalk.red("Rosetta 2 is NOT installed. The npm version of protoc is not compatible with Apple Silicon."), - ) - console.log(chalk.cyan("Please install Rosetta 2 using the following command:")) - console.log(chalk.cyan(" softwareupdate --install-rosetta --agree-to-license")) - console.log(chalk.red("Aborting build process.")) - process.exit(1) - } else { - console.log(chalk.green("Rosetta 2 is installed. Continuing with build.")) - } - } catch (error) { - console.log(chalk.yellow("Could not determine Rosetta installation status. Proceeding anyway.")) - } - } -} - const __filename = fileURLToPath(import.meta.url) const SCRIPT_DIR = path.dirname(__filename) const ROOT_DIR = path.resolve(SCRIPT_DIR, "..") @@ -89,11 +57,9 @@ async function main() { // Define output directories const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto") - const HOST_TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto", "host") // Create output directories if they don't exist await fs.mkdir(TS_OUT_DIR, { recursive: true }) - await fs.mkdir(HOST_TS_OUT_DIR, { recursive: true }) // Clean up existing generated files console.log(chalk.cyan("Cleaning up existing generated TypeScript files...")) @@ -105,9 +71,9 @@ async function main() { // Check for missing proto files for services in serviceNameMap await ensureProtoFilesExist() - // Process main proto files + // Process all proto files console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR) - const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR, realpath: true }) + const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR, realpath: true }) // Build the protoc command with proper path handling for cross-platform const tsProtocCommand = [ @@ -115,6 +81,8 @@ async function main() { `--proto_path="${SCRIPT_DIR}"`, `--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`, `--ts_proto_out="${TS_OUT_DIR}"`, + "--ts_proto_opt=exportCommonSymbols=false", + "--ts_proto_opt=outputIndex=true", "--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages", ...protoFiles, ].join(" ") @@ -126,42 +94,15 @@ async function main() { process.exit(1) } - // Process host proto files - console.log(chalk.cyan("Processing host proto files from"), path.join(SCRIPT_DIR, "host")) - const hostProtoFiles = await globby("*.proto", { cwd: path.join(SCRIPT_DIR, "host"), absolute: true }) - - if (hostProtoFiles.length > 0) { - // Build the protoc command for host proto files - const hostTsProtocCommand = [ - protoc, - `--proto_path="${SCRIPT_DIR}"`, - `--proto_path="${path.join(SCRIPT_DIR, "host")}"`, - `--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`, - `--ts_proto_out="${TS_OUT_DIR}"`, - "--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages", - ...hostProtoFiles, - ].join(" ") - try { - console.log(chalk.cyan(`Generating TypeScript code for host proto files:\n${hostProtoFiles.join("\n")}...`)) - execSync(hostTsProtocCommand, { stdio: "inherit" }) - } catch (error) { - console.error(chalk.red("Error generating TypeScript for host proto files:"), error) - process.exit(1) - } - } - const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto") await fs.mkdir(descriptorOutDir, { recursive: true }) - const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb") - const allProtoFiles = [...protoFiles, ...hostProtoFiles] const descriptorProtocCommand = [ protoc, `--proto_path="${SCRIPT_DIR}"`, - `--proto_path="${path.join(SCRIPT_DIR, "host")}"`, `--descriptor_set_out="${descriptorFile}"`, "--include_imports", - ...allProtoFiles, + ...protoFiles, ].join(" ") try { console.log(chalk.cyan("Generating descriptor set...")) @@ -702,6 +643,36 @@ export { console.log(chalk.green(`Generated host gRPC client at ${configPath}`)) } +// Check for Apple Silicon compatibility +function checkAppleSiliconCompatibility() { + // Only run check on macOS + if (process.platform !== "darwin") { + return + } + + // Check if running on Apple Silicon + const cpuArchitecture = os.arch() + if (cpuArchitecture === "arm64") { + try { + // Check if Rosetta is installed + const rosettaCheck = execSync('/usr/bin/pgrep oahd || echo "NOT_INSTALLED"').toString().trim() + + if (rosettaCheck === "NOT_INSTALLED") { + console.log(chalk.yellow("Detected Apple Silicon (ARM64) architecture.")) + console.log( + chalk.red("Rosetta 2 is NOT installed. The npm version of protoc is not compatible with Apple Silicon."), + ) + console.log(chalk.cyan("Please install Rosetta 2 using the following command:")) + console.log(chalk.cyan(" softwareupdate --install-rosetta --agree-to-license")) + console.log(chalk.red("Aborting build process.")) + process.exit(1) + } + } catch (error) { + console.log(chalk.yellow("Could not determine Rosetta installation status. Proceeding anyway.")) + } + } +} + // Run the main function main().catch((error) => { console.error(chalk.red("Error:"), error) diff --git a/scripts/generate-server-setup.mjs b/scripts/generate-server-setup.mjs index 2e17d9b138..1b6b484168 100644 --- a/scripts/generate-server-setup.mjs +++ b/scripts/generate-server-setup.mjs @@ -32,13 +32,15 @@ function generateHandlersAndExports() { handlerSetup.push(` server.addService(proto.cline.${name}.service, {`) for (const [rpcName, rpc] of Object.entries(def.service)) { imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`) + const requestType = "proto.cline." + rpc.requestType.type.name if (rpc.requestStream) { throw new Error("Request streaming is not supported") } if (rpc.responseStream) { - handlerSetup.push(` ${rpcName}: wrapStreamingResponse(${rpcName}, controller),`) + handlerSetup.push(` ${rpcName}: wrapStreamingResponse<${requestType},void>(${rpcName}, controller),`) } else { - handlerSetup.push(` ${rpcName}: wrapper(${rpcName}, controller),`) + const responseType = "proto.cline." + rpc.responseType.type.name + handlerSetup.push(` ${rpcName}: wrapper<${requestType},${responseType}>(${rpcName}, controller),`) } } handlerSetup.push(` });`) @@ -58,11 +60,11 @@ const scriptName = path.basename(fileURLToPath(import.meta.url)) let output = `// GENERATED CODE -- DO NOT EDIT! // Generated by ${scriptName} import * as grpc from "@grpc/grpc-js" +import * as proto from "@/shared/proto" import { Controller } from "../core/controller" import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types" ${imports} - export function addServices( server: grpc.Server, proto: any, diff --git a/src/core/controller/ui/subscribeToAccountButtonClicked.ts b/src/core/controller/ui/subscribeToAccountButtonClicked.ts index 3f66c8410b..b980908e4a 100644 --- a/src/core/controller/ui/subscribeToAccountButtonClicked.ts +++ b/src/core/controller/ui/subscribeToAccountButtonClicked.ts @@ -46,7 +46,7 @@ export async function sendAccountButtonClickedEvent(controllerId: string): Promi } try { - const event: Empty = {} + const event: Empty = Empty.create({}) await responseStream(event, false) } catch (error) { console.error(`Error sending account button clicked event to controller ${controllerId}:`, error)