Created
May 27, 2023 11:21
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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(); | |
} | |
}); | |
Author
piksel
commented
May 27, 2023
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment