import { Logger } from "@gnu-taler/taler-util"; import chokidar from "chokidar"; import express from "express"; import https from "https"; import http from "http"; import { parse } from "url"; import WebSocket from "ws"; import locahostCrt from "./keys/localhost.crt"; import locahostKey from "./keys/localhost.key"; import storiesHtml from "./stories.html"; import path from "path"; const httpServerOptions = { key: locahostKey, cert: locahostCrt, }; const logger = new Logger("serve.ts"); const PATHS = { WS: "/ws", EXAMPLE: "/examples", APP: "/app", }; export async function serve(opts: { folder: string; port: number; source?: string; tls?: boolean; examplesLocationJs?: string; examplesLocationCss?: string; onSourceUpdate?: () => Promise; }): Promise { const app = express(); app.use(PATHS.APP, express.static(opts.folder)); const httpServer = http.createServer(app); const httpPort = opts.port; let httpsServer: typeof httpServer | undefined; let httpsPort: number | undefined; const servers = [httpServer]; if (opts.tls) { httpsServer = https.createServer(httpServerOptions, app); httpsPort = opts.port + 1; servers.push(httpsServer) } logger.info(`Dev server. Endpoints:`); logger.info(` ${PATHS.APP}: where root application can be tested`); logger.info(` ${PATHS.EXAMPLE}: where examples can be found and browse`); logger.info(` ${PATHS.WS}: websocket for live reloading`); const wss = new WebSocket.Server({ noServer: true }); wss.on("connection", function connection(ws) { ws.send("welcome"); }); servers.forEach(function addWSHandler(server) { server.on("upgrade", function upgrade(request, socket, head) { const { pathname } = parse(request.url || ""); if (pathname === PATHS.WS) { wss.handleUpgrade(request, socket, head, function done(ws) { wss.emit("connection", ws, request); }); } else { socket.destroy(); } }); }); const sendToAllClients = function (data: object): void { wss.clients.forEach(function each(client) { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(data)); } }); }; const watchingFolder = opts.source ?? opts.folder; logger.info(`watching ${watchingFolder} for changes`); chokidar.watch(watchingFolder).on("change", (path, stats) => { logger.info(`changed: ${path}`); if (opts.onSourceUpdate) { sendToAllClients({ type: "file-updated-start", data: { path } }); opts .onSourceUpdate() .then((result) => { sendToAllClients({ type: "file-updated-done", data: { path, result }, }); }) .catch((error) => { sendToAllClients({ type: "file-updated-failed", data: { path, error: JSON.stringify(error) }, }); }); } else { sendToAllClients({ type: "file-change", data: { path } }); } }); if (opts.onSourceUpdate) opts.onSourceUpdate(); app.get(PATHS.EXAMPLE, function (req: any, res: any) { res.set("Content-Type", "text/html"); res.send( storiesHtml .replace( "__EXAMPLES_JS_FILE_LOCATION__", opts.examplesLocationJs ?? `.${PATHS.APP}/stories.js`, ) .replace( "__EXAMPLES_CSS_FILE_LOCATION__", opts.examplesLocationCss ?? `.${PATHS.APP}/stories.css`, ), ); }); logger.info(`Serving ${opts.folder} on ${httpPort}: plain HTTP`); httpServer.listen(httpPort); if (httpsServer !== undefined) { logger.info(`Serving ${opts.folder} on ${httpsPort}: HTTP + TLS`); httpsServer.listen(httpsPort); } }