dify/packages/dev-proxy/src/cli.ts

162 lines
4.6 KiB
TypeScript

import type { ServerType } from '@hono/node-server'
import type { DevProxyCliOptions, DevProxyConfig } from './types'
import process from 'node:process'
import { serve } from '@hono/node-server'
import { watch } from 'chokidar'
import { assertDevProxyConfig, loadDevProxyConfig, parseDevProxyCliArgs, resolveDevProxyServerOptions, watchDevProxyConfig } from './config'
import { createDevProxyApp } from './server'
function printUsage() {
console.log(`Usage:
dev-proxy --config <path> [options]
Options:
--config, -c <path> Path to a dev proxy config file. Defaults to dev-proxy.config.ts.
--env-file <path> Load environment variables before evaluating the config file.
--host <host> Override the configured host.
--port <port> Override the configured port.
--watch Reload config and env file changes. Enabled by default.
--no-watch Disable config and env file reloads.
--help, -h Show this help message.`)
}
async function flushStandardStreams() {
await Promise.all([
new Promise<void>(resolve => process.stdout.write('', () => resolve())),
new Promise<void>(resolve => process.stderr.write('', () => resolve())),
])
}
const closeServer = (server: ServerType) => new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error)
reject(error)
else
resolve()
})
})
const startDevProxyServer = (config: DevProxyConfig, cliOptions: DevProxyCliOptions) => {
let app = createDevProxyApp(config)
const { host, port } = resolveDevProxyServerOptions(config.server, cliOptions)
const server = serve({
fetch: (request, env) => app.fetch(request, env),
hostname: host,
port,
})
return {
host,
port,
server,
updateConfig(nextConfig: DevProxyConfig) {
app = createDevProxyApp(nextConfig)
},
}
}
const createDevProxyRuntime = (initialConfig: DevProxyConfig, cliOptions: DevProxyCliOptions) => {
let runtime = startDevProxyServer(initialConfig, cliOptions)
let reloadTask = Promise.resolve()
console.log(`[dev-proxy] listening on http://${runtime.host}:${runtime.port}`)
const reload = async (nextConfig: unknown, reason: string) => {
assertDevProxyConfig(nextConfig)
const nextServerOptions = resolveDevProxyServerOptions(nextConfig.server, cliOptions)
if (runtime.host === nextServerOptions.host && runtime.port === nextServerOptions.port) {
runtime.updateConfig(nextConfig)
console.log(`[dev-proxy] reloaded ${reason}`)
return
}
await closeServer(runtime.server)
runtime = startDevProxyServer(nextConfig, cliOptions)
console.log(`[dev-proxy] restarted on http://${runtime.host}:${runtime.port} after ${reason}`)
}
const enqueueReload = (loadConfig: () => Promise<unknown> | unknown, reason: string) => {
reloadTask = reloadTask.then(async () => {
try {
await reload(await loadConfig(), reason)
}
catch (error) {
console.error(`[dev-proxy] failed to reload ${reason}`)
console.error(error instanceof Error ? error.message : error)
}
})
return reloadTask
}
return {
enqueueReload,
close: async () => {
await reloadTask
await closeServer(runtime.server)
},
}
}
async function main() {
const cliOptions = parseDevProxyCliArgs(process.argv.slice(2))
if (cliOptions.help) {
printUsage()
return
}
const config = await loadDevProxyConfig(cliOptions.config, process.cwd(), {
envFile: cliOptions.envFile,
})
const runtime = createDevProxyRuntime(config, cliOptions)
if (cliOptions.watch === false)
return
const configWatcher = await watchDevProxyConfig(cliOptions.config, process.cwd(), {
envFile: cliOptions.envFile,
onUpdate: ({ newConfig }) => runtime.enqueueReload(() => newConfig.config, 'config changes'),
})
const envWatcher = cliOptions.envFile
? watch(cliOptions.envFile, {
cwd: process.cwd(),
ignoreInitial: true,
})
: undefined
envWatcher?.on('all', () => {
void runtime.enqueueReload(
() => loadDevProxyConfig(cliOptions.config, process.cwd(), {
envFile: cliOptions.envFile,
}),
'env file changes',
)
})
const cleanup = async () => {
await envWatcher?.close()
await configWatcher.unwatch()
await runtime.close()
}
process.once('SIGINT', () => {
void cleanup().finally(() => process.exit(0))
})
process.once('SIGTERM', () => {
void cleanup().finally(() => process.exit(0))
})
}
try {
await main()
await flushStandardStreams()
}
catch (error) {
console.error(error instanceof Error ? error.message : error)
await flushStandardStreams()
process.exit(1)
}