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'}`)
})