244 lines
7.9 KiB
JavaScript
Executable File
244 lines
7.9 KiB
JavaScript
Executable File
#!/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)
|
|
})
|
|
}
|