import type { FastifyInstance, RouteHandler } from 'fastify' import fmiddie from '@fastify/middie' import { createServer } from 'vite' import StatusError from '../../lib/status_error.ts' import type { Route } from '../../../shared/types.ts' import type { Entry, ParsedConfig, Renderer, RenderFunction } from './types.ts' export default async function viteDevelopment(fastify: FastifyInstance, config: ParsedConfig) { const devServer = await createServer({ server: { middlewareMode: true }, appType: 'custom', }) fastify.decorate('devServer', devServer) fastify.decorateReply('ctx', null) await fastify.register(fmiddie) fastify.use(devServer.middlewares) for (const entry of Object.values(config.entries)) { fastify.register((fastify, _, done) => setupEntry(fastify, entry).then(() => done()), { prefix: entry.path, }) } fastify.addHook('onClose', () => devServer.close()) } async function setupEntry(fastify: FastifyInstance, entry: Entry) { const renderer = createRenderer(fastify, entry) 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) } const routes: Route[] = entry.routes || (await fastify.devServer.ssrLoadModule(`/${entry.name}/server.ts`)).routes for (const route of routes.flatMap((route) => route.routes || route)) { // const preHandler = entry.preHandler || entry.createPreHandler?.(route, entry) 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, preHandler, }) } } else { fastify.route({ method: 'GET', url: route.path, handler, preHandler, }) } } fastify.setNotFoundHandler(() => { throw new StatusError(404) }) if (entry.createErrorHandler) { fastify.setErrorHandler( entry.createErrorHandler(async (error, request, reply) => { reply.type('text/html').status((error as StatusError).status || 500) return renderer( request.url, Object.assign( { error, }, reply.ctx, ), ) }), ) } } function createRenderer(fastify: FastifyInstance, entry: Entry): Renderer { return async (url, ctx) => { ctx = Object.assign({ url }, entry.ctx, ctx) const { render } = (await fastify.devServer.ssrLoadModule(`/${entry.name}/server.ts`)) as { render?: RenderFunction } const renderPromise = render && entry.ssr !== false ? render(ctx) : null return fastify.devServer.transformIndexHtml( url, await entry .template({ env: 'development', preloads: [], script: `/${entry.name}/client.ts`, styles: [], content: renderPromise?.then((result) => result.html), head: renderPromise?.then((result) => result.head), state: renderPromise?.then((result) => result.state) || Promise.resolve(ctx), }) .text(), ) } } function createHandler(renderer: Renderer): RouteHandler { return async function handler(request, reply) { reply.type('text/html') return renderer(request.url, reply.ctx) } }