// This is a polyglot entrypoint for Headplane when running in production // It doesn't use any dependencies aside from @remix-run/node and mime // During build we bundle the used dependencies into the file so that // we can only need this file and a Node.js installation to run the server. // PREFIX is defined globally, see vite.config.ts import { access, constants } from 'node:fs/promises'; import { createReadStream, existsSync, statSync } from 'node:fs'; import { createServer } from 'node:http'; import { join, resolve } from 'node:path'; import { env } from 'node:process'; import { log } from './utils.mjs'; import { getWss, registerWss } from './ws.mjs'; import { createReadableStreamFromReadable, writeReadableStreamToWritable, } from '@react-router/node'; log('SRVX', 'INFO', `Running with Node.js ${process.versions.node}`); try { await access('./node_modules/react-router', constants.F_OK | constants.R_OK); log('SRVX', 'INFO', 'Found node_modules dependencies'); } catch (error) { log('SRVX', 'ERROR', 'No node_modules found. Please run `pnpm install`'); log('SRVX', 'ERROR', error); process.exit(1); } const { createRequestHandler } = await import('react-router'); const { default: mime } = await import('mime'); const port = env.PORT || 3000; const host = env.HOST || '0.0.0.0'; const buildPath = env.BUILD_PATH || './build'; const baseDir = resolve(join(buildPath, 'client')); if (!global.BUILD) { try { await access(join(buildPath, 'server'), constants.F_OK | constants.R_OK); log('SRVX', 'INFO', 'Found build directory'); } catch (error) { const date = new Date().toISOString(); log('SRVX', 'ERROR', 'No build found. Please run `pnpm build`'); log('SRVX', 'ERROR', error); process.exit(1); } // Because this is a dynamic import without an easily discernable path // we gain the "deoptimization" we want so that Vite doesn't bundle this const build = await import(resolve(join(buildPath, 'server', 'index.js'))); global.BUILD = build; global.MODE = 'production'; } const handler = createRequestHandler(global.BUILD, global.MODE); const http = createServer(async (req, res) => { const url = new URL(`http://${req.headers.host}${req.url}`); if (global.MIDDLEWARE) { await new Promise((resolve) => { global.MIDDLEWARE(req, res, resolve); }); } if (!url.pathname.startsWith(PREFIX)) { res.writeHead(404); res.end(); return; } // We need to handle an issue where say we are navigating to $PREFIX // but Remix does not handle it without the trailing slash. This is // because Remix uses the URL constructor to parse the URL and it // will remove the trailing slash. We need to redirect to the correct // URL so that Remix can handle it correctly. if (url.pathname === PREFIX) { res.writeHead(302, { Location: `${PREFIX}/`, }); res.end(); return; } // Before we pass any requests to our Remix handler we need to check // if we can handle a raw file request. This is important for the // Remix loader to work correctly. // // To optimize this, we send them as readable streams in the node // response and we also set headers for aggressive caching. if (url.pathname.startsWith(`${PREFIX}/assets/`)) { const filePath = join(baseDir, url.pathname.replace(PREFIX, '')); const exists = existsSync(filePath); const stats = statSync(filePath); if (exists && stats.isFile()) { // Build assets are cache-bust friendly so we can cache them heavily if (req.url.startsWith('/build')) { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); } // Send the file as a readable stream const fileStream = createReadStream(filePath); const type = mime.getType(filePath); res.setHeader('Content-Length', stats.size); res.setHeader('Content-Type', type); fileStream.pipe(res); return; } } // Handling the request const controller = new AbortController(); res.on('close', () => controller.abort()); const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (!value) continue; if (Array.isArray(value)) { for (const v of value) { headers.append(key, v); } continue; } headers.append(key, value); } const remixReq = new Request(url.href, { headers, method: req.method, signal: controller.signal, // If we have a body we set a duplex and we load the body ...(req.method !== 'GET' && req.method !== 'HEAD' ? { body: createReadableStreamFromReadable(req), duplex: 'half', } : {}), }); // Pass our request to the Remix handler and get a response const response = await handler(remixReq, { ws: getWss(), }); // Handle our response and reply res.statusCode = response.status; res.statusMessage = response.statusText; for (const [key, value] of response.headers.entries()) { res.appendHeader(key, value); } if (response.body) { await writeReadableStreamToWritable(response.body, res); return; } res.end(); }); registerWss(http); http.listen(port, host, () => { log('SRVX', 'INFO', `Running on ${host}:${port}`); });