Skip to content

Instantly share code, notes, and snippets.

@piksel
Created May 27, 2023 11:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save piksel/e89647e0222591595fb4276c10267b59 to your computer and use it in GitHub Desktop.
Save piksel/e89647e0222591595fb4276c10267b59 to your computer and use it in GitHub Desktop.
Node script that serves files from a directory, injecting a script into HTML pages that reloads the page whenever a POST request to the file completes
#!/usr/bin/env node
/*
Serves files from a directory, injecting a script into HTML pages that reloads the page whenever a POST request to the file completes.
Requires Node.js v18+
Usage:
node http-serve-autoreload.mjs [ROOT-PATH] [PORT]
*/
import { createServer } from 'node:http';
import * as path from 'node:path';
import { readFile, access, watch } from 'node:fs/promises';
const [,,argPath,argPort] = process.argv;
const serveRoot = path.resolve(argPath ?? '.');
const servePort = argPort ? parseInt(argPort, 10) : 8000;
const allowedFileTypes = {html: 'text/html', htm: 'text/html', css: 'text/css', js: 'text/javascript', png: 'image/png', svg: 'image/svg+xml'};
const injectScript = '<script>fetch(location.href, {method: "POST"}).then(_ => history.go(0))</script>';
const fileExists = file => access(file).then(() => true, () => false);
const pendingReqs = new Map();
createServer(async (req, res) => {
const start = new Date().getTime();
const response = (status, body, headers) => {
console.log(`${new Date().toISOString()} ${req.method.padEnd(4)} %o %o in %oms`, req.url, status, new Date().getTime() - start);
res.writeHead(status, headers);
res.end(body);
}
if (req.method !== 'GET' && req.method !== 'POST') return response(400, `Method ${req.method} not supported`);
const filePath = path.join(serveRoot, req.url.replace(/\?.+$/, ''));
if (!await fileExists(filePath))
return response(404);
const fileMime = allowedFileTypes[path.extname(filePath).replace(/^\./, '')];
if (!fileMime)
return response(400, 'File type not supported');
if (req.method === 'POST') {
if (!pendingReqs.has(filePath)) {
pendingReqs.set(filePath, new Set());
}
const pendSet = pendingReqs.get(filePath) ?? new Set();
const event = await new Promise(resolve => pendSet.add(resolve));
console.log(`${new Date().toISOString()} Got event %o for %o`, event, filePath);
return response(204);
}
const fileBody = await readFile(filePath, 'utf-8');
const body = (fileMime === 'text/html') ? injectScript + fileBody : fileBody;
return response(200, body, { 'Content-Type': fileMime, 'Content-Length': Buffer.byteLength(body) });
}).listen(servePort, async () => {
console.log('Serving files from %o, listening on port %o...', serveRoot, servePort);
for await (const event of watch(serveRoot, { persistent: false, recursive: true })) {
const filePending = pendingReqs.get(path.join(serveRoot, event.filename));
if (!filePending) continue;
for(const resolve of filePending.values()) {
resolve(event.eventType);
}
filePending.clear();
}
});
@piksel
Copy link
Author

piksel commented May 27, 2023

❯ http-serve-autoreload.mjs
Serving files from '/tmp/http-serve-auto-example', listening on port 8000...
2023-05-27T11:41:18.547Z GET  '/example.html' 200 in 1ms      # Initial load with injected script
2023-05-27T11:41:41.854Z Got event 'change' for '/tmp/http-serve-auto-example/example.html'
2023-05-27T11:41:41.855Z POST '/example.html' 204 in 23184ms  # File updated 23s after start
2023-05-27T11:41:41.878Z GET  '/example.html' 200 in 1ms      # Browser reloaded page

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment