aboutsummaryrefslogtreecommitdiff
path: root/packages/web-util/src/serve.ts
blob: 3d2744bb91356131c0dceb59e2155720bc37380a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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;
  examplesLocationJs?: string;
  examplesLocationCss?: string;
  onSourceUpdate?: () => Promise<void>;
}): Promise<void> {
  const app = express();

  app.use(PATHS.APP, express.static(opts.folder));

  const httpServer = http.createServer(app);
  const httpPort = opts.port;
  const httpsServer = https.createServer(httpServerOptions, app);
  const httpsPort = opts.port + 1;
  const servers = [httpServer, 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);
  logger.info(`Serving ${opts.folder} on ${httpsPort}: HTTP + TLS`);
  httpsServer.listen(httpsPort);
}