import fs from 'node:fs/promises' import express from 'express' import cookieParser from 'cookie-parser' import { compress } from 'compress-json' const isProd = process.env.NODE_ENV !== 'development' const port = process.env.PORT || 5174 const base = process.env.BASE || '/' const MESSAGE = { 404: 'Not Found', 0: 'Unknown' } const templateHtml = isProd ? await fs.readFile('./dist/client/index.html', 'utf-8') : '' const app = express() app.set('trust proxy', true) app.use(cookieParser()) let vite if (!isProd) { const { createServer } = await import('vite') vite = await createServer({ server: { middlewareMode: true }, appType: 'custom', base, }) app.use(vite.middlewares) } else { const sirv = (await import('sirv')).default app.use(base, sirv('./dist/client', { extensions: [] })) } function generateHead({ title, metas }) { const heads = [`${title}`] for (const meta of metas) { const attrs = Object.entries(meta).map(([k, v]) => `${k}="${v}"`).join(' ') heads.push(``) } return heads.join('') } function injectHTML(template, { head, state }) { return template .replace('', head) .replace('', ``) } app.use('*all', async (req, res) => { try { const url = req.originalUrl.replace(base, '') console.log(`${req.ip} /${url} "${req.get('User-Agent')}"`) let template, render, getRoute if (!isProd) { template = await fs.readFile('./index.html', 'utf-8') template = await vite.transformIndexHtml(url, template) const mod = await vite.ssrLoadModule('/src/entry-server.js') render = mod.render getRoute = mod.getRoute } else { template = templateHtml const mod = await import('./dist/server/entry-server.js') render = mod.render getRoute = mod.getRoute } const { router, code, title, metas, meta } = await getRoute(url) if (code !== 200 && !req.accepts('html')) { res.status(code).type('text').send(MESSAGE[code] || MESSAGE[0]) return } const { stream, piniaState, headState } = await render(router, req.cookies, req.headers.host) const [htmlStart, htmlEnd] = template.split('') if (meta) { const buffer = [] let headSent = false for await (const chunk of stream) { if (res.closed) break if (headSent) { res.write(chunk) } else { if (headState.ready) { const head = generateHead({ title: headState.title || title, metas: [...metas, ...headState.meta], }) res.status(headState.code || code).type('html') res.write(htmlStart.replace('', head)) buffer.forEach(c => res.write(c)) res.write(chunk) headSent = true } else { buffer.push(chunk) } } } if (!headState.ready) { console.warn('[WARN] meta not ready, falling back to default meta') const head = generateHead({ title, metas }) res.status(code).type('html') res.write(htmlStart.replace('', head)) for await (const chunk of buffer) { if (res.closed) break res.write(chunk) } } } else { const head = generateHead({ title, metas }) res.status(code).type('html') res.write(htmlStart.replace('', head)) for await (const chunk of stream) { if (res.closed) break res.write(chunk) } } const stateScript = JSON.stringify(compress(piniaState)) res.write(htmlEnd.replace('', ``)) res.end() } catch (err) { vite?.ssrFixStacktrace(err) console.error('[ERROR]', err.stack || err) res.status(500).end(err.stack) } }) app.listen(port, () => { console.log(`🚀 Server running at http://localhost:${port} in mode ${isProd ? 'production' : 'development'}`) })