generate-host-bridge-client.mjs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. #!/usr/bin/env node
  2. import chalk from "chalk"
  3. import * as path from "path"
  4. import { writeFileWithMkdirs } from "./file-utils.mjs"
  5. import { getFqn, loadServicesFromProtoDescriptor } from "./proto-utils.mjs"
  6. // Contains the interface definitions for the host bridge clients.
  7. const TYPES_FILE = path.resolve("src/generated/hosts/host-bridge-client-types.ts")
  8. // Contains the ExternalHostBridgeClientManager for the external host bridge clients (using nice-grpc).
  9. const EXTERNAL_CLIENT_FILE = path.resolve("src/generated/hosts/standalone/host-bridge-clients.ts")
  10. // Contains the handler map for the external host bridge clients (using the custom service registry).
  11. const VSCODE_CLIENT_FILE = path.resolve("src/generated/hosts/vscode/hostbridge-grpc-service-config.ts")
  12. /**
  13. * Main function to generate the host bridge client
  14. */
  15. export async function main() {
  16. const { hostServices } = await loadServicesFromProtoDescriptor()
  17. await generateTypesFile(hostServices)
  18. await generateExternalClientFile(hostServices)
  19. await generateVscodeClientFile(hostServices)
  20. console.log(`Generated Host Bridge client files at:`)
  21. console.log(`- ${TYPES_FILE}`)
  22. console.log(`- ${EXTERNAL_CLIENT_FILE}`)
  23. console.log(`- ${VSCODE_CLIENT_FILE}`)
  24. }
  25. /**
  26. * Generate the client interfaces file.
  27. */
  28. async function generateTypesFile(hostServices) {
  29. const clientInterfaces = []
  30. for (const [name, def] of Object.entries(hostServices)) {
  31. const clientInterface = generateClientInterfaceType(name, def)
  32. clientInterfaces.push(clientInterface)
  33. }
  34. const content = `// GENERATED CODE -- DO NOT EDIT!
  35. // Generated by scripts/generate-host-bridge-client.mjs
  36. import * as proto from "@shared/proto/index"
  37. import { StreamingCallbacks } from "@hosts/host-provider-types"
  38. ${clientInterfaces.join("\n\n")}
  39. `
  40. // Write output file
  41. await writeFileWithMkdirs(TYPES_FILE, content)
  42. }
  43. /**
  44. * Generate a client interface for a service.
  45. */
  46. function generateClientInterfaceType(serviceName, serviceDefinition) {
  47. // Get the methods from the service definition
  48. const methods = Object.entries(serviceDefinition.service)
  49. .map(([methodName, methodDef]) => {
  50. const requestType = getFqn(methodDef.requestType.type.name)
  51. const responseType = getFqn(methodDef.responseType.type.name)
  52. if (!methodDef.responseStream) {
  53. // Generate unary method signature.
  54. return ` ${methodName}(request: ${requestType}): Promise<${responseType}>;`
  55. }
  56. // Generate streaming method signature.
  57. return ` ${methodName}(request: ${requestType}, callbacks: StreamingCallbacks<${responseType}>): () => void;`
  58. })
  59. .join("\n\n")
  60. // Generate the interface
  61. return `/**
  62. * Interface for ${serviceName} client.
  63. */
  64. export interface ${serviceName}ClientInterface {
  65. ${methods}
  66. }`
  67. }
  68. /**
  69. * Generate the external client implementations file.
  70. */
  71. async function generateExternalClientFile(hostServices) {
  72. // Generate imports
  73. const imports = []
  74. // Add imports for the interfaces
  75. for (const [name, _def] of Object.entries(hostServices)) {
  76. imports.push(`import { ${name}ClientInterface } from "@generated/hosts/host-bridge-client-types"`)
  77. }
  78. const clientImplementations = []
  79. for (const [name, def] of Object.entries(hostServices)) {
  80. clientImplementations.push(generateExternalClientSetup(name, def))
  81. }
  82. const content = `// GENERATED CODE -- DO NOT EDIT!
  83. // Generated by scripts/generate-host-bridge-client.mjs
  84. import { asyncIteratorToCallbacks } from "@/standalone/utils"
  85. import * as niceGrpc from "@generated/nice-grpc/index"
  86. import { StreamingCallbacks } from "@hosts/host-provider-types"
  87. import * as proto from "@shared/proto/index"
  88. import { Channel, createClient } from "nice-grpc"
  89. import { BaseGrpcClient } from "@/hosts/external/grpc-types"
  90. ${imports.join("\n")}
  91. ${clientImplementations.join("\n\n")}
  92. `
  93. // Write output file
  94. await writeFileWithMkdirs(EXTERNAL_CLIENT_FILE, content)
  95. }
  96. /**
  97. * Generate a client implementation class for a service
  98. */
  99. function generateExternalClientSetup(serviceName, serviceDefinition) {
  100. // Get the methods from the service definition
  101. const methods = Object.entries(serviceDefinition.service)
  102. .map(([methodName, methodDef]) => {
  103. // Get fully qualified type names
  104. const requestType = getFqn(methodDef.requestType.type.name)
  105. const responseType = getFqn(methodDef.responseType.type.name)
  106. const isStreamingResponse = methodDef.responseStream
  107. if (!isStreamingResponse) {
  108. return ` ${methodName}(request: ${requestType}): Promise<${responseType}> {
  109. return this.makeRequest((client) => client.${methodName}(request))
  110. }`
  111. } else {
  112. // Generate streaming method
  113. return ` ${methodName}(
  114. request: ${requestType},
  115. callbacks: StreamingCallbacks<${responseType}>,
  116. ): () => void {
  117. const client = this.getClient()
  118. const abortController = new AbortController()
  119. const stream: AsyncIterable<${responseType}> = client.${methodName}(request, {
  120. signal: abortController.signal,
  121. })
  122. const wrappedCallbacks: StreamingCallbacks<${responseType}> = {
  123. ...callbacks,
  124. onError: (error: any) => {
  125. if (error?.code === "UNAVAILABLE") {
  126. this.destroyClient()
  127. }
  128. callbacks.onError?.(error)
  129. },
  130. }
  131. asyncIteratorToCallbacks(stream, wrappedCallbacks)
  132. return () => {
  133. abortController.abort()
  134. }
  135. }\n`
  136. }
  137. })
  138. .join("\n")
  139. // Generate the class
  140. return `/**
  141. * Type-safe client implementation for ${serviceName}.
  142. */
  143. export class ${serviceName}ClientImpl
  144. extends BaseGrpcClient<niceGrpc.host.${serviceName}Client>
  145. implements ${serviceName}ClientInterface {
  146. protected createClient(channel: Channel): niceGrpc.host.${serviceName}Client {
  147. return createClient(niceGrpc.host.${serviceName}Definition, channel)
  148. }
  149. ${methods}
  150. }`
  151. }
  152. /**
  153. * Generate the Vscode client setup file.
  154. */
  155. async function generateVscodeClientFile(hostServices) {
  156. const imports = []
  157. const clientImplementations = []
  158. const handlerMap = []
  159. for (const [serviceName, serviceDefinition] of Object.entries(hostServices)) {
  160. const name = serviceName.replace(/Service$/, "").toLowerCase()
  161. for (const [methodName, _methodDef] of Object.entries(serviceDefinition.service)) {
  162. imports.push(`import { ${methodName} } from "@/hosts/vscode/hostbridge/${name}/${methodName}"`)
  163. }
  164. imports.push("")
  165. clientImplementations.push(generateVscodeClientImplementation(name, serviceDefinition))
  166. handlerMap.push(` "host.${serviceName}": {
  167. requestHandler: ${name}ServiceRegistry.handleRequest,
  168. streamingHandler: ${name}ServiceRegistry.handleStreamingRequest,
  169. },`)
  170. }
  171. const content = `// GENERATED CODE -- DO NOT EDIT!
  172. // Generated by scripts/generate-host-bridge-client.mjs
  173. import { createServiceRegistry } from "@hosts/vscode/hostbridge-grpc-service"
  174. import { HostServiceHandlerConfig } from "@hosts/vscode/hostbridge-grpc-handler"
  175. ${imports.join("\n")}
  176. ${clientImplementations.join("\n\n")}
  177. /**
  178. * Map of host service names to their handler configurations
  179. */
  180. export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {
  181. ${handlerMap.join("\n")}
  182. }
  183. `
  184. // Write output file
  185. await writeFileWithMkdirs(VSCODE_CLIENT_FILE, content)
  186. }
  187. function generateVscodeClientImplementation(serviceName, serviceDefinition) {
  188. // Get the methods from the service definition
  189. const name = serviceName.replace(/Service$/, "").toLowerCase()
  190. const methods = Object.entries(serviceDefinition.service)
  191. .map(([methodName, methodDef]) => {
  192. // Get fully qualified type names
  193. const isStreamingResponse = methodDef.responseStream
  194. if (!isStreamingResponse) {
  195. return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName})`
  196. } else {
  197. return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName}, { isStreaming: true })`
  198. }
  199. })
  200. .join("\n")
  201. // Generate the class
  202. return `// Setup ${name} service registry
  203. const ${name}ServiceRegistry = createServiceRegistry("${name}")
  204. ${methods}`
  205. }
  206. // Only run main if this script is executed directly
  207. if (import.meta.url === `file://${process.argv[1]}`) {
  208. main().catch((error) => {
  209. console.error(chalk.red("Error:"), error)
  210. process.exit(1)
  211. })
  212. }