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 [options] Options: --config, -c Path to a dev proxy config file. Defaults to dev-proxy.config.ts. --env-file Load environment variables before evaluating the config file. --host Override the configured host. --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(resolve => process.stdout.write('', () => resolve())), new Promise(resolve => process.stderr.write('', () => resolve())), ]) } const closeServer = (server: ServerType) => new Promise((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, 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) }