import fs from 'fs' import path from 'path' import http from 'http' import getPort from 'get-port' import sirv from 'sirv' import chokidar from 'chokidar' import mime from 'mime-types' import toRegExp from 'regexparam' import { timer } from './timer' import * as logger from './log' import { default404 } from './default404' import { requestToEvent } from './requestToEvent' import { sendServerlessResponse } from './sendServerlessResponse' import { createLiveReloadScript } from './liveReloadScript' import { AWS, Presta } from './types' function resolveHTML(dir: string, url: string) { let file = path.join(dir, url) // if no extension, it's probably intended to be an HTML file if (!path.extname(url)) { try { return fs.readFileSync(path.join(dir, url, 'index.html'), 'utf8') } catch (e) {} } return fs.readFileSync(file, 'utf8') } export function createServerHandler({ port, config }: { port: number; config: Presta }) { const devClient = createLiveReloadScript({ port }) const staticDir = config.staticOutputDir const assetDir = config.assets return async function serveHandler(req: http.IncomingMessage, res: http.ServerResponse) { const time = timer() const url = req.url as string /* * If this is an asset other than HTML files, just serve it */ logger.debug({ label: 'debug', message: `attempting to serve user static asset ${url}`, }) /* * first check the vcs-tracked static folder, * then check the presta-built static folder * * @see https://github.com/sure-thing/presta/issues/30 */ sirv(assetDir, { dev: true })(req, res, () => { logger.debug({ label: 'debug', message: `attempting to serve generated static asset ${url}`, }) sirv(staticDir, { dev: true })(req, res, async () => { try { /* * No asset file, no static file, try dynamic */ delete require.cache[config.functionsManifest] const manifest = require(config.functionsManifest) const routes = Object.keys(manifest) const lambdaFilepath = routes .map((route) => ({ matcher: toRegExp(route), route, })) .filter(({ matcher }) => { return matcher.pattern.test(url.split('?')[0]) }) .map(({ route }) => manifest[route])[0] /** * If we have a serverless function, delegate to it, otherwise 404 */ if (lambdaFilepath) { logger.debug({ label: 'debug', message: `attempting to render lambda for ${url}`, }) const { handler }: { handler: AWS['Handler'] } = require(lambdaFilepath) const event = await requestToEvent(req) const response = await handler(event, {}) const headers = response.headers || {} const redir = response.statusCode > 299 && response.statusCode < 399 // get mime type const type = headers['Content-Type'] as string const ext = type ? mime.extension(type) : 'html' logger.info({ label: 'serve', message: `${response.statusCode} ${redir ? headers.Location : url}`, duration: time(), }) sendServerlessResponse(res, { statusCode: response.statusCode, headers: response.headers, multiValueHeaders: response.multiValueHeaders, // only html can be live-reloaded, duh body: ext === 'html' ? (response.body || '').split('')[0] + devClient : response.body, }) } else { logger.debug({ label: 'debug', message: `attempting to render static 404.html page for ${url}`, }) /* * Try to fall back to a static 404 page */ try { const file = resolveHTML(staticDir, '404') + devClient logger.warn({ label: 'serve', message: `404 ${url}`, duration: time(), }) sendServerlessResponse(res, { statusCode: 404, body: file, }) } catch (e) { if (!(e as Error).message.includes('ENOENT')) { console.error(e) } logger.debug({ label: 'debug', message: `rendering default 404 HTML page for ${url}`, }) logger.warn({ label: 'serve', message: `404 ${url}`, duration: time(), }) sendServerlessResponse(res, { statusCode: 404, body: default404 + devClient, }) } } } catch (e) { logger.debug({ label: 'debug', message: `rendering default 500 HTML page for ${url}`, }) logger.error({ label: 'serve', message: `500 ${url}`, error: e as Error, duration: time(), }) sendServerlessResponse(res, { statusCode: 500, body: '' + devClient, // TODO default 500 screen }) } }) }) } } export async function serve(config: Presta) { const port = await getPort({ port: config.port }) const server = http.createServer(createServerHandler({ port, config })).listen(port) const socket = require('pocket.io')(server, { serveClient: false }) config.hooks.onBrowserRefresh(() => { logger.debug({ label: 'debug', message: `refresh event received`, }) socket.emit('refresh') }) chokidar.watch(config.assets, { ignoreInitial: true }).on('all', () => { config.hooks.emitBrowserRefresh() }) return { port } }