| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- #!/usr/bin/env node
- import chalk from "chalk"
- import * as path from "path"
- import { writeFileWithMkdirs } from "./file-utils.mjs"
- import { getFqn, loadServicesFromProtoDescriptor } from "./proto-utils.mjs"
- // Contains the interface definitions for the host bridge clients.
- const TYPES_FILE = path.resolve("src/generated/hosts/host-bridge-client-types.ts")
- // Contains the ExternalHostBridgeClientManager for the external host bridge clients (using nice-grpc).
- const EXTERNAL_CLIENT_FILE = path.resolve("src/generated/hosts/standalone/host-bridge-clients.ts")
- // Contains the handler map for the external host bridge clients (using the custom service registry).
- const VSCODE_CLIENT_FILE = path.resolve("src/generated/hosts/vscode/hostbridge-grpc-service-config.ts")
- /**
- * Main function to generate the host bridge client
- */
- export async function main() {
- const { hostServices } = await loadServicesFromProtoDescriptor()
- await generateTypesFile(hostServices)
- await generateExternalClientFile(hostServices)
- await generateVscodeClientFile(hostServices)
- console.log(`Generated Host Bridge client files at:`)
- console.log(`- ${TYPES_FILE}`)
- console.log(`- ${EXTERNAL_CLIENT_FILE}`)
- console.log(`- ${VSCODE_CLIENT_FILE}`)
- }
- /**
- * Generate the client interfaces file.
- */
- async function generateTypesFile(hostServices) {
- const clientInterfaces = []
- for (const [name, def] of Object.entries(hostServices)) {
- const clientInterface = generateClientInterfaceType(name, def)
- clientInterfaces.push(clientInterface)
- }
- const content = `// GENERATED CODE -- DO NOT EDIT!
- // Generated by scripts/generate-host-bridge-client.mjs
- import * as proto from "@shared/proto/index"
- import { StreamingCallbacks } from "@hosts/host-provider-types"
- ${clientInterfaces.join("\n\n")}
- `
- // Write output file
- await writeFileWithMkdirs(TYPES_FILE, content)
- }
- /**
- * Generate a client interface for a service.
- */
- function generateClientInterfaceType(serviceName, serviceDefinition) {
- // Get the methods from the service definition
- const methods = Object.entries(serviceDefinition.service)
- .map(([methodName, methodDef]) => {
- const requestType = getFqn(methodDef.requestType.type.name)
- const responseType = getFqn(methodDef.responseType.type.name)
- if (!methodDef.responseStream) {
- // Generate unary method signature.
- return ` ${methodName}(request: ${requestType}): Promise<${responseType}>;`
- }
- // Generate streaming method signature.
- return ` ${methodName}(request: ${requestType}, callbacks: StreamingCallbacks<${responseType}>): () => void;`
- })
- .join("\n\n")
- // Generate the interface
- return `/**
- * Interface for ${serviceName} client.
- */
- export interface ${serviceName}ClientInterface {
- ${methods}
- }`
- }
- /**
- * Generate the external client implementations file.
- */
- async function generateExternalClientFile(hostServices) {
- // Generate imports
- const imports = []
- // Add imports for the interfaces
- for (const [name, _def] of Object.entries(hostServices)) {
- imports.push(`import { ${name}ClientInterface } from "@generated/hosts/host-bridge-client-types"`)
- }
- const clientImplementations = []
- for (const [name, def] of Object.entries(hostServices)) {
- clientImplementations.push(generateExternalClientSetup(name, def))
- }
- const content = `// GENERATED CODE -- DO NOT EDIT!
- // Generated by scripts/generate-host-bridge-client.mjs
- import { asyncIteratorToCallbacks } from "@/standalone/utils"
- import * as niceGrpc from "@generated/nice-grpc/index"
- import { StreamingCallbacks } from "@hosts/host-provider-types"
- import * as proto from "@shared/proto/index"
- import { Channel, createClient } from "nice-grpc"
- import { BaseGrpcClient } from "@/hosts/external/grpc-types"
- ${imports.join("\n")}
- ${clientImplementations.join("\n\n")}
- `
- // Write output file
- await writeFileWithMkdirs(EXTERNAL_CLIENT_FILE, content)
- }
- /**
- * Generate a client implementation class for a service
- */
- function generateExternalClientSetup(serviceName, serviceDefinition) {
- // Get the methods from the service definition
- const methods = Object.entries(serviceDefinition.service)
- .map(([methodName, methodDef]) => {
- // Get fully qualified type names
- const requestType = getFqn(methodDef.requestType.type.name)
- const responseType = getFqn(methodDef.responseType.type.name)
- const isStreamingResponse = methodDef.responseStream
- if (!isStreamingResponse) {
- return ` ${methodName}(request: ${requestType}): Promise<${responseType}> {
- return this.makeRequest((client) => client.${methodName}(request))
- }`
- } else {
- // Generate streaming method
- return ` ${methodName}(
- request: ${requestType},
- callbacks: StreamingCallbacks<${responseType}>,
- ): () => void {
- const client = this.getClient()
- const abortController = new AbortController()
- const stream: AsyncIterable<${responseType}> = client.${methodName}(request, {
- signal: abortController.signal,
- })
- const wrappedCallbacks: StreamingCallbacks<${responseType}> = {
- ...callbacks,
- onError: (error: any) => {
- if (error?.code === "UNAVAILABLE") {
- this.destroyClient()
- }
- callbacks.onError?.(error)
- },
- }
- asyncIteratorToCallbacks(stream, wrappedCallbacks)
- return () => {
- abortController.abort()
- }
- }\n`
- }
- })
- .join("\n")
- // Generate the class
- return `/**
- * Type-safe client implementation for ${serviceName}.
- */
- export class ${serviceName}ClientImpl
- extends BaseGrpcClient<niceGrpc.host.${serviceName}Client>
- implements ${serviceName}ClientInterface {
- protected createClient(channel: Channel): niceGrpc.host.${serviceName}Client {
- return createClient(niceGrpc.host.${serviceName}Definition, channel)
- }
- ${methods}
- }`
- }
- /**
- * Generate the Vscode client setup file.
- */
- async function generateVscodeClientFile(hostServices) {
- const imports = []
- const clientImplementations = []
- const handlerMap = []
- for (const [serviceName, serviceDefinition] of Object.entries(hostServices)) {
- const name = serviceName.replace(/Service$/, "").toLowerCase()
- for (const [methodName, _methodDef] of Object.entries(serviceDefinition.service)) {
- imports.push(`import { ${methodName} } from "@/hosts/vscode/hostbridge/${name}/${methodName}"`)
- }
- imports.push("")
- clientImplementations.push(generateVscodeClientImplementation(name, serviceDefinition))
- handlerMap.push(` "host.${serviceName}": {
- requestHandler: ${name}ServiceRegistry.handleRequest,
- streamingHandler: ${name}ServiceRegistry.handleStreamingRequest,
- },`)
- }
- const content = `// GENERATED CODE -- DO NOT EDIT!
- // Generated by scripts/generate-host-bridge-client.mjs
- import { createServiceRegistry } from "@hosts/vscode/hostbridge-grpc-service"
- import { HostServiceHandlerConfig } from "@hosts/vscode/hostbridge-grpc-handler"
- ${imports.join("\n")}
- ${clientImplementations.join("\n\n")}
- /**
- * Map of host service names to their handler configurations
- */
- export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {
- ${handlerMap.join("\n")}
- }
- `
- // Write output file
- await writeFileWithMkdirs(VSCODE_CLIENT_FILE, content)
- }
- function generateVscodeClientImplementation(serviceName, serviceDefinition) {
- // Get the methods from the service definition
- const name = serviceName.replace(/Service$/, "").toLowerCase()
- const methods = Object.entries(serviceDefinition.service)
- .map(([methodName, methodDef]) => {
- // Get fully qualified type names
- const isStreamingResponse = methodDef.responseStream
- if (!isStreamingResponse) {
- return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName})`
- } else {
- return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName}, { isStreaming: true })`
- }
- })
- .join("\n")
- // Generate the class
- return `// Setup ${name} service registry
- const ${name}ServiceRegistry = createServiceRegistry("${name}")
- ${methods}`
- }
- // Only run main if this script is executed directly
- if (import.meta.url === `file://${process.argv[1]}`) {
- main().catch((error) => {
- console.error(chalk.red("Error:"), error)
- process.exit(1)
- })
- }
|