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