76 行
3.1 KiB
JavaScript
76 行
3.1 KiB
JavaScript
import { createReadStream } from 'node:fs'
|
|
import { readFile, stat } from 'node:fs/promises'
|
|
import { createServer } from 'node:http'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import { render } from './dist/server/entry-server.js'
|
|
|
|
const root = fileURLToPath (new URL ('./dist/client/', import.meta.url))
|
|
const template = await readFile (path.join (root, 'index.html'), 'utf8')
|
|
const origin = new URL (process.env.PUBLIC_ORIGIN ?? 'http://localhost:5173').origin
|
|
const apiBaseURL = process.env.SSR_API_BASE_URL ?? 'http://127.0.0.1:3002'
|
|
const types = {
|
|
'.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json',
|
|
'.png': 'image/png', '.gif': 'image/gif', '.jpg': 'image/jpeg',
|
|
'.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.webp': 'image/webp',
|
|
'.woff': 'font/woff', '.woff2': 'font/woff2', '.otf': 'font/otf',
|
|
'.mp4': 'video/mp4', '.mp3': 'audio/mpeg', '.txt': 'text/plain' }
|
|
|
|
if (process.env.NODE_ENV === 'production' && !process.env.PUBLIC_ORIGIN)
|
|
throw new Error ('PUBLIC_ORIGIN is required in production')
|
|
|
|
createServer (async (request, response) => {
|
|
response.setHeader ('Cache-Control', 'no-store')
|
|
if (!['GET', 'HEAD'].includes (request.method))
|
|
{
|
|
response.writeHead (405, { Allow: 'GET, HEAD' }).end ()
|
|
return
|
|
}
|
|
try
|
|
{
|
|
const url = new URL (request.url, origin)
|
|
const pathname = decodeURIComponent (url.pathname)
|
|
const file = path.resolve (root, '.' + pathname)
|
|
const allowed = file.startsWith (root)
|
|
&& !pathname.split ('/').some (segment => segment.startsWith ('.'))
|
|
&& pathname !== '/index.html'
|
|
const info = allowed ? await stat (file).catch (() => null) : null
|
|
if (info?.isFile ())
|
|
{
|
|
response.writeHead (200, {
|
|
'Content-Type': types[path.extname (file)] ?? 'application/octet-stream',
|
|
'Content-Length': info.size,
|
|
'Cache-Control': /\/[^/]+-[\w-]{8}\.[^.]+$/.test (pathname)
|
|
? 'public, max-age=31536000, immutable' : 'public, max-age=3600' })
|
|
if (request.method === 'HEAD')
|
|
response.end ()
|
|
else
|
|
createReadStream (file).on ('error', () => response.destroy ()).pipe (response)
|
|
return
|
|
}
|
|
if (pathname.startsWith ('/assets/'))
|
|
{
|
|
response.writeHead (404).end ()
|
|
return
|
|
}
|
|
const result = await render (url.pathname + url.search, origin, apiBaseURL)
|
|
const html = result == null ? template : template
|
|
.replace ('lang="ja"', `lang="${result.locale}"`)
|
|
.replace (/<title>[\s\S]*?<\/title>/, '')
|
|
.replace (/<meta name="description"[^>]*>/, '')
|
|
.replace ('<!-- helmet -->', () => result.head)
|
|
.replace ('<!-- outlet -->', () => result.html)
|
|
.replace ('<!-- ssr-state -->',
|
|
() => `<script id="ssr-state" type="application/json">${result.bootstrap}</script>`)
|
|
response.writeHead (result?.status ?? 200, {
|
|
'Content-Type': 'text/html; charset=utf-8' })
|
|
response.end (request.method === 'HEAD' ? undefined : html)
|
|
}
|
|
catch (error)
|
|
{
|
|
console.error (error)
|
|
response.writeHead (error instanceof URIError ? 400 : 503).end ()
|
|
}
|
|
}).listen (Number (process.env.PORT ?? 5173), process.env.HOST ?? '127.0.0.1')
|