brf/server/plugins/vite/production.ts
2025-12-09 12:00:39 +01:00

165 lines
4.6 KiB
TypeScript

import { type FastifyInstance } from 'fastify'
import path from 'node:path'
import fs from 'node:fs'
import fstatic from '@fastify/static'
import { resolveConfig } from 'vite'
import StatusError from '../../lib/status_error.ts'
import { type Entry, type ParsedConfig } from '../vite.ts'
export default async function viteProduction(fastify: FastifyInstance, config: ParsedConfig) {
const viteConfig = await resolveConfig({}, 'build', 'production')
fastify.register(fstatic, {
root: viteConfig.build.outDir,
wildcard: false,
})
fastify.decorateReply('ctx', null)
for (const entry of Object.values(config.entries)) {
fastify.register((fastify, _, done) => setupEntry(fastify, entry, config, viteConfig).then(() => done()), {
prefix: entry.path,
})
}
}
async function setupEntry(fastify: FastifyInstance, entry: Entry, config: ParsedConfig, viteConfig) {
const viteFilePath = path.join(viteConfig.build.outDir, entry.name + '.js')
const viteFile = fs.existsSync(viteFilePath)
? await import(viteFilePath)
: await import(path.join(viteConfig.root, entry.name, 'server.ts'))
const manifestPath = path.join(viteConfig.build.outDir, `manifest.${entry.name}.json`)
const manifest = fs.existsSync(manifestPath) ? (await import(manifestPath, { with: { type: 'json' } })).default : null
const routes = entry.routes || viteFile.routes
const cache = new Map()
const renderer = createRenderer(fastify, entry, viteFile.render, manifest)
const cachedHandler = createCachedHandler(cache)
const handler = createHandler(renderer)
if (entry.preHandler || entry.createPreHandler) {
fastify.addHook('onRequest', (_request, reply, done) => {
reply.ctx = Object.create(null)
done()
})
}
if (entry.preHandler) {
fastify.addHook('preHandler', entry.preHandler)
}
for (const route of routes.flatMap((route) => route.routes || route)) {
const preHandler = entry.createPreHandler?.(route, entry)
if (route.locales?.length) {
const locales = [{ hostname: entry.hostname, path: route.path }, ...route.locales]
for (const locale of locales) {
fastify.route({
method: 'GET',
url: locale.path,
onRequest(request, reply, done) {
if (request.hostname !== locale.hostname) {
// TODO should probably redirect to correct path on request.hostname, not to locale.hostname
return reply.redirect('http://' + locale.hostname + request.url)
}
done()
},
handler: route.cache ? cachedHandler : handler,
preHandler,
})
}
} else {
fastify.route({
method: 'GET',
url: route.path,
handler: route.cache ? cachedHandler : handler,
preHandler,
})
}
}
entry.setupRebuildCache?.(() => buildCache(entry, routes, renderer, cache))
fastify.setNotFoundHandler(() => {
throw new StatusError(404)
})
fastify.setErrorHandler(
entry.createErrorHandler(async (error, request, reply) => {
reply.type('text/html').status(error.status || 500)
return renderer(
request.url,
Object.assign(
{
error,
},
reply.ctx,
),
)
}),
)
}
function createRenderer(fastify, entry, render, manifest) {
const files = manifest[`${entry.name}/client.ts`]
const bundle = path.join('/', files.file)
const preload = files.imports?.map((name) => path.join('/', manifest[name].file))
const css = files.css?.map((name) => path.join('/', name))
return (url, ctx) => {
ctx = Object.assign({ url }, entry.ctx, ctx)
const renderPromise = render?.(ctx)
return entry
.template({
env: 'production',
script: bundle,
preload,
css,
content: renderPromise?.then((result) => result.content),
head: renderPromise?.then((result) => result.head),
state: renderPromise?.then((result) => result.state) || Promise.resolve(ctx),
})
.stream(true)
}
}
function createHandler(renderer) {
return async function handler(request, reply) {
reply.type('text/html')
return renderer(request.url, reply.ctx)
}
}
function createCachedHandler(cache) {
return function cachedHandler(request, reply) {
reply.type('text/html')
return cache.get(request.url)
}
}
async function buildCache(entry, routes, renderer, cache) {
for (const route of routes) {
if (route.cache) {
let html = ''
// TODO run preHandlers
for await (const chunk of renderer(route.path)) {
html += chunk
}
cache.set(route.path, html)
}
}
}