srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/web/serve.mjs
blob: 64d6339f4a1facab36417dcf0d0d57b51e6e5afa (plain) (blame)
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
#!/usr/bin/env node
// Zero-dependency static file server for the TyperPunk web app.
// Replaces the Vite dev server so the web app has no npm runtime dependency.
import { createServer } from 'node:http';
import { readFile, stat } from 'node:fs/promises';
import { extname, join, normalize, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';

const ROOT = resolve(fileURLToPath(new URL('.', import.meta.url)));
const PORT = Number(process.env.PORT) || 4173;
const HOST = process.env.HOST || '0.0.0.0';

const MIME_TYPES = {
    '.html': 'text/html; charset=utf-8',
    '.js': 'text/javascript; charset=utf-8',
    '.mjs': 'text/javascript; charset=utf-8',
    '.css': 'text/css; charset=utf-8',
    '.json': 'application/json; charset=utf-8',
    '.wasm': 'application/wasm',
    '.svg': 'image/svg+xml',
    '.png': 'image/png',
    '.ico': 'image/x-icon',
};

async function resolveFile(urlPath) {
    let decoded;
    try {
        decoded = decodeURIComponent(urlPath.split('?')[0]);
    } catch {
        return null;
    }
    const safePath = normalize(join(ROOT, decoded));
    if (safePath !== ROOT && !safePath.startsWith(ROOT + sep)) return null;

    let target = safePath;
    try {
        const info = await stat(target);
        if (info.isDirectory()) target = join(target, 'index.html');
    } catch {
        return null;
    }

    try {
        await stat(target);
        return target;
    } catch {
        return null;
    }
}

const server = createServer(async (req, res) => {
    const filePath = (await resolveFile(req.url)) || (await resolveFile('/index.html'));
    if (!filePath) {
        res.writeHead(404, { 'Content-Type': 'text/plain' });
        res.end('Not found');
        return;
    }
    try {
        const body = await readFile(filePath);
        const type = MIME_TYPES[extname(filePath)] || 'application/octet-stream';
        // This replaces a dev server, and the app is under active
        // development - no cache headers at all left the browser free to
        // hold onto old JS/CSS indefinitely with no way to tell it changed,
        // which has already caused a real "why don't I see the fix" report.
        res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'no-cache' });
        res.end(body);
    } catch (err) {
        res.writeHead(500, { 'Content-Type': 'text/plain' });
        res.end('Internal server error');
        console.error(err);
    }
});

server.listen(PORT, HOST, () => {
    console.log(`TyperPunk web running at http://localhost:${PORT}`);
});