UNPKG

hotweby

Version:

Automatic hot-reloading webserver using websockets

213 lines (186 loc) 5.71 kB
import express from "express" import * as afs from "fs/promises" import { IncomingMessage, Server, ServerResponse } from "http" import * as path from "path" import { WebSocket, WebSocketServer } from "ws" import { TriggerHandler } from "./index.js" export function createReloadHtmlCode() { const reloadHtmlScript = async () => { console.log("[HOTWEBY]: connect websocket...") const connectWs = () => { const wsUrl = (location.protocol === "https:" ? "wss://" : "ws://") + location.host const ws = new WebSocket(wsUrl) ws.onopen = () => console.log("[HOTWEBY]: websocket connected") ws.onclose = () => { console.log( "[HOTWEBY]: websocket closed, reloading...", ) setTimeout(() => location.reload(), 100) } ws.onerror = err => { console.error( "[HOTWEBY]: websocket error, reloading...", err, ) setTimeout(() => location.reload(), 1000) } } connectWs() } const reloadHtmlCode = "" + reloadHtmlScript return "<script>\n(" + reloadHtmlCode + ")()\n</script>" } export function createExpress( targetDir: string, reloadHtmlCode: string, autoExtensionResolution: boolean, verbose: boolean, ) { const app = express() app.use(async (req, res, next) => { if ( !req.path.endsWith(".html") && !req.path.endsWith("/") ) { return next() } let reqPath = req.path if (reqPath.endsWith("/")) { reqPath += "index.html" } try { const data = await afs.readFile( targetDir + reqPath, "utf8", ) res.status(200) res.send(reloadHtmlCode + "\n" + data.toString()) verbose && console.info( "Served html-file '" + req.path + "' from '" + targetDir + reqPath + "'", ) } catch (err) { console.error( "Cant read requested html-file '" + req.path + "'" + "\nfrom '" + targetDir + reqPath + "':\n", err, ) res.status(503) res.setHeader("Content-Type", "text/plain") res.setHeader("Retry-After", "5") res.send( "Cant read requested html-file '" + req.path + "'", ) } }) autoExtensionResolution && app.use(async (req, res, next) => { const resolvedExtension = await autoResolveExtensions( req.url, targetDir, verbose, ) if (resolvedExtension) { req.url = resolvedExtension } next() }) app.use(express.static(targetDir)) return app } export async function autoResolveExtensions( reqUrl: string, targetDir: string, verbose: boolean, ): Promise<string | undefined> { while (reqUrl.startsWith("/")) { reqUrl = reqUrl.slice(1) } if (reqUrl === "") { return undefined } try { let realPath = targetDir + "/" + reqUrl if ((await pathType(realPath)) === "none") { const files = await afs.readdir( path.dirname(realPath), ) const baseName = path.basename(realPath) for (const file of files) { if (file.startsWith(baseName + ".")) { verbose && console.info( "Auto resolved extension for '" + reqUrl + "' to be '" + path.dirname(reqUrl) + "/" + file + "'", ) return path.dirname(reqUrl) + "/" + file } } } } catch (err) { verbose && console.error( "Error while auto resolving extension for " + reqUrl + "\n", err, ) } return undefined } export async function pathType( path: string, ): Promise<"file" | "dir" | "none"> { try { const stat = await afs.stat(path) return stat.isFile() ? "file" : "dir" } catch {} return "none" } export function createWebSocketServer( httpServer: Server< typeof IncomingMessage, typeof ServerResponse >, registerTrigger: (triggerHandler: TriggerHandler) => void, ) { const wsServer = new WebSocketServer({ noServer: true }) wsServer.on("connection", (ws, req) => { registerTrigger(() => { if ( ws.readyState === ws.OPEN || ws.readyState === ws.CONNECTING ) { ws.close() } }) }) httpServer.on("upgrade", (req, socket, head) => { wsServer.handleUpgrade(req, socket, head, ws => { wsServer.emit("connection", ws, req) }) }) return wsServer }