index.js (4074B)
1 import bodyParser from 'body-parser'; 2 import cors from 'cors'; 3 import fs from 'node:fs'; 4 import http from 'node:http'; 5 import path from 'node:path'; 6 import { fileURLToPath, pathToFileURL } from 'node:url'; 7 import morgan from 'morgan'; 8 import scandir from '@finwo/scandir'; 9 import serveStatic from 'serve-static'; 10 11 import { app } from './app.js'; 12 import { loadConfig } from './lib/config.js'; 13 import { closeRealms, initRealms } from './lib/realm.js'; 14 15 const here = path.dirname(fileURLToPath(import.meta.url)); 16 const dist = path.join(here, '..', 'web', 'dist'); 17 18 const cfg = loadConfig(); 19 app.config = cfg; 20 21 async function loadModules(dir) { 22 const files = []; 23 scandir.sync(dir, (file) => files.push(file), ['js'], false); 24 files.sort(); 25 const modules = []; 26 for (const file of files) { 27 modules.push(await import(pathToFileURL(file).href)); 28 } 29 return modules; 30 } 31 32 for (const mod of await loadModules(path.join(here, 'lib', 'http'))) { 33 Object.assign(app, mod); 34 } 35 36 app.use(morgan('tiny')); 37 app.use(cors()); 38 app.use(bodyParser.json({ limit: '16kb' })); 39 app.use(bodyParser.urlencoded({ extended: false, limit: '16kb' })); 40 41 const routes = []; 42 for (const mod of await loadModules(path.join(here, 'controller'))) { 43 const exported = mod.default ?? mod.route ?? mod; 44 for (const route of Array.isArray(exported) ? exported : [exported]) { 45 if (!route) continue; 46 if (route.mount) { 47 app.use(route.mount, route.handler); 48 continue; 49 } 50 if (!route.method || !route.path) continue; 51 if (route.name) { 52 app.manifest.push({ 53 method: route.method, 54 path: route.path, 55 name: route.name, 56 }); 57 } 58 routes.push(route); 59 } 60 } 61 62 routes.sort((A, B) => { 63 const aparts = A.path.split('/'); 64 const bparts = B.path.split('/'); 65 for (let i = 0; i < Math.min(aparts.length, bparts.length); i += 1) { 66 if (aparts[i].startsWith(':') && !bparts[i].startsWith(':')) return 1; 67 if (!aparts[i].startsWith(':') && bparts[i].startsWith(':')) return -1; 68 } 69 return bparts.length - aparts.length; 70 }); 71 72 for (const route of routes) { 73 app[route.method](route.path, async (req, res, ...args) => { 74 try { 75 const result = await route.handler(req, res, ...args); 76 if (result instanceof app.HttpResponse) result.send(res); 77 } catch (e) { 78 if (res.headersSent) { 79 res.end(); 80 return; 81 } 82 if (e instanceof app.HttpError) { 83 e.toResponse().send(res); 84 return; 85 } 86 console.error(e); 87 new app.HttpError(500, 'internal_error', 'Internal server error').toResponse().send(res); 88 } 89 }); 90 } 91 92 const serve = serveStatic(dist, { 93 index: ['index.htm', 'index.html'], 94 }); 95 96 let indexContent = null; 97 try { 98 indexContent = fs.readFileSync(path.join(dist, 'index.html')); 99 } catch { 100 indexContent = null; 101 } 102 103 await initRealms(cfg); 104 105 const server = http.createServer((req, res) => { 106 app(req, res, (err) => { 107 if (err) { 108 const status = err.status || err.statusCode || 500; 109 const code = err.type === 'entity.parse.failed' ? 'invalid_json' : 'internal_error'; 110 res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' }); 111 res.end(JSON.stringify({ error: { code, message: err.message || 'Request failed' } })); 112 return; 113 } 114 115 serve(req, res, () => { 116 if (!indexContent) { 117 res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' }); 118 res.end('frontend build missing'); 119 return; 120 } 121 res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); 122 res.end(indexContent); 123 }); 124 }); 125 }); 126 127 server.listen(cfg.server.port, cfg.server.host, () => { 128 console.log(`Listening on ${cfg.server.host}:${cfg.server.port}`); 129 }); 130 131 let shuttingDown = false; 132 133 async function shutdown(signal) { 134 if (shuttingDown) return; 135 shuttingDown = true; 136 console.log(`${signal} received, shutting down`); 137 server.close(); 138 await closeRealms(); 139 } 140 141 for (const signal of ['SIGINT', 'SIGTERM']) { 142 process.once(signal, () => { 143 shutdown(signal).finally(() => process.exit(0)); 144 }); 145 }