index.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { existsSync } from 'node:fs'
  2. import { resolve } from 'node:path'
  3. import send from 'koa-send'
  4. import serve from 'koa-static'
  5. import { createApplication, createNodeServer } from './app.js'
  6. import { loadServerConfig } from './config.js'
  7. import { DashboardSupervisor } from './dashboardSupervisor.js'
  8. import { isLocalAuthorizationTarget } from './loopbackAuthorization.js'
  9. const config = loadServerConfig()
  10. const runtime = createApplication({
  11. config,
  12. })
  13. const dashboardSupervisor = config.superviseDashboard
  14. && isLocalAuthorizationTarget(config.upstream) && config.upstream.hostname === '127.0.0.1'
  15. && config.upstream.protocol === 'http:' && config.upstream.port === '9119'
  16. ? new DashboardSupervisor({})
  17. : undefined
  18. const nodeRuntime = createNodeServer(runtime)
  19. let closeFrontend = async (): Promise<void> => undefined
  20. if (runtime.config.production) {
  21. const dist = resolve(process.cwd(), 'dist')
  22. runtime.app.use(serve(dist, { index: 'index.html' }))
  23. runtime.app.use(async (ctx) => {
  24. if (ctx.method !== 'GET' || !ctx.accepts('html') || !existsSync(resolve(dist, 'index.html'))) {
  25. ctx.status = 404
  26. return
  27. }
  28. await send(ctx, 'index.html', { root: dist })
  29. })
  30. } else {
  31. const { createServer } = await import('vite')
  32. const vite = await createServer({
  33. server: {
  34. middlewareMode: true,
  35. ws: { server: nodeRuntime.server },
  36. },
  37. appType: 'spa',
  38. })
  39. closeFrontend = () => vite.close()
  40. runtime.app.use(async (ctx) => {
  41. const handled = await new Promise<boolean>((resolvePromise, reject) => {
  42. const finish = () => {
  43. cleanup()
  44. resolvePromise(true)
  45. }
  46. const cleanup = () => {
  47. ctx.res.off('finish', finish)
  48. ctx.res.off('close', finish)
  49. }
  50. ctx.res.once('finish', finish)
  51. ctx.res.once('close', finish)
  52. vite.middlewares(ctx.req, ctx.res, (error?: unknown) => {
  53. cleanup()
  54. if (error) reject(error)
  55. else resolvePromise(false)
  56. })
  57. })
  58. if (handled) ctx.respond = false
  59. else ctx.status = 404
  60. })
  61. }
  62. nodeRuntime.server.once('error', (error: NodeJS.ErrnoException) => {
  63. if (error.code === 'EADDRINUSE') {
  64. console.error(`夭夭 Web could not start: ${runtime.config.host}:${runtime.config.port} is already in use`)
  65. } else {
  66. console.error(`夭夭 Web could not start: ${error.message}`)
  67. }
  68. process.exitCode = 1
  69. })
  70. nodeRuntime.server.listen(runtime.config.port, runtime.config.host, () => {
  71. const protocol = runtime.config.tlsCert ? 'https' : 'http'
  72. console.log(`夭夭 Web listening on ${protocol}://${runtime.config.host}:${runtime.config.port}`)
  73. if (runtime.config.insecureLan) {
  74. console.warn('Warning: trusted-LAN HTTP mode is enabled; credentials are not encrypted in transit.')
  75. }
  76. dashboardSupervisor?.start()
  77. })
  78. let closing = false
  79. async function shutdown(): Promise<void> {
  80. if (closing) return
  81. closing = true
  82. try {
  83. dashboardSupervisor?.stop()
  84. await closeFrontend()
  85. await nodeRuntime.close()
  86. } finally {
  87. process.exitCode = 0
  88. }
  89. }
  90. process.once('SIGINT', () => { void shutdown() })
  91. process.once('SIGTERM', () => { void shutdown() })