Files
ao3-mirror-ssr/server.js

131 lines
4.0 KiB
JavaScript

import fs from 'node:fs/promises'
import express from 'express'
import cookieParser from 'cookie-parser'
import { compress } from 'compress-json'
const isProduction = process.env.NODE_ENV === 'production'
const port = process.env.PORT || 5173
const base = process.env.BASE || '/'
const templateHtml = isProduction
? await fs.readFile('./dist/client/index.html', 'utf-8')
: ''
const app = express()
app.use(cookieParser());
const MESSAGE = {
404: 'Not Found',
0: 'Unknown'
}
let vite
if (!isProduction) {
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: [] }))
}
app.set('trust proxy', true);
app.use('*all', async (req, res) => {
try {
const url = req.originalUrl.replace(base, '')
const ua = req.get('User-Agent');
console.log(`${req.ip} /${url} "${ua}"`)
let template
let render, getRoute
if (!isProduction) {
// Always read fresh template in development
template = await fs.readFile('./index.html', 'utf-8')
template = await vite.transformIndexHtml(url, template)
const module = await vite.ssrLoadModule('/src/entry-server.js')
render = module.render
getRoute = module.getRoute
} else {
template = templateHtml
const module = await import('./dist/server/entry-server.js')
render = module.render
getRoute = module.getRoute
}
const { router, code, title, metas, meta } = await getRoute(url)
if (code != 200 && !req.accepts('html')) {
res.status(code).set({ 'Content-Type': 'text/plain' })
res.write(MESSAGE[code] || MESSAGE[0])
res.end()
return
}
const { stream, piniaState, headState } = await render(router, req.cookies, req.headers.host)
const [htmlStart, htmlEnd] = template.split('<!--app-html-->')
if (meta) {
const buffer = []
let headReady = false
for await (const chunk of stream) {
if (res.closed) break
if (headReady) res.write(chunk)
else {
if (headState.ready) {
res.status(headState.code || code).set({ 'Content-Type': 'text/html' })
const heads = [`<title>${ headState.title || title }</title>`]
for (const item of [ ...metas, ...headState.meta ]) {
const properties = []
for (const [key, value] of Object.entries(item)) properties.push(`${key}="${value}"`)
heads.push(`<meta ${properties.join(' ')}>`)
}
res.write(htmlStart.replace('<!--app-head-->',heads.join('')))
for (const item of buffer) res.write(item)
res.write(chunk)
headReady = true
} else buffer.push(chunk)
}
}
if (!headState.ready) {
console.warn('Page not set meta ready! No stream render at all!')
const heads = [`<title>${ title }</title>`]
for (const item of metas) {
const properties = []
for (const [key, value] of Object.entries(item)) properties.push(`${key}="${value}"`)
heads.push(`<meta ${properties.join(' ')}>`)
}
res.write(htmlStart.replace('<!--app-head-->',heads.join('')))
for await (const chunk of buffer) {
if (res.closed) break
res.write(chunk)
}
}
} else {
res.status(code).set({ 'Content-Type': 'text/html' })
const heads = [`<title>${ title }</title>`]
for (const item of metas) {
const properties = []
for (const [key, value] of Object.entries(item)) properties.push(`${key}="${value}"`)
heads.push(`<meta ${properties.join(' ')}>`)
}
res.write(htmlStart.replace('<!--app-head-->',heads.join('')))
for await (const chunk of stream) {
if (res.closed) break
res.write(chunk)
}
}
const piniaStateContent = JSON.stringify(compress(piniaState))
const stateScript = `<script>window.__PINIA_STATE__=${piniaStateContent}</script>`
res.write(htmlEnd.replace('<!--app-state-->', stateScript))
res.end()
} catch (e) {
vite?.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})
app.listen(port, () => {
console.log(`Server started at port ${port}`)
})