Skip to content

Instantly share code, notes, and snippets.

@gordonnl
Created May 19, 2020 07:55
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 gordonnl/795c0ef00d09f7f31134851f009ac809 to your computer and use it in GitHub Desktop.
Save gordonnl/795c0ef00d09f7f31134851f009ac809 to your computer and use it in GitHub Desktop.
Node development server
// Based on https://github.com/lukejacksonn/servor
// For direct use
// node server ${process.cwd()} 8080 --debug
// node server ${process.cwd()}
const args = process.argv.slice(2).filter((x) => x.indexOf('-') !== 0);
const root = args[0];
const port = args[1];
const debug = ~process.argv.indexOf('--debug') || ~process.argv.indexOf('-d');
const fs = require('fs');
const parse = require('url').parse;
const join = require('path').join;
const http = require('http');
const networkInterfaces = require('os').networkInterfaces;
const net = require('net');
const execSync = require('child_process').execSync;
const mimeTypes = require('./lib/types.json');
// Get available IP addresses
const ips = Object.values(networkInterfaces())
.reduce((every, i) => [...every, ...i], [])
.filter((i) => i.family === 'IPv4' && i.internal === false)
.map((i) => i.address);
// Generate map of all known mimetypes
const mime = Object.entries(mimeTypes).reduce(
(all, [type, exts]) => Object.assign(all, ...exts.map((ext) => ({ [ext]: type }))),
{}
);
// Start the server on the desired port
async function serve({ root, port, fallback = 'index.html', runtime } = {}) {
port = port || (await fport());
const clients = [];
// Start the server on the desired port
http.createServer((req, res) => {
const url = parse(req.url);
const isRoute = isRouteRequest(url.pathname);
// redirect to trailing slash
if (isRoute && !url.pathname.endsWith('/')) {
url.pathname += '/';
res.statusCode = 301;
res.setHeader('Location', url.format());
return res.end();
}
// Check static file or index.html
let resource = decodeURI(url.pathname + (isRoute ? fallback : ''));
let uri = join(root, resource);
let status = 200;
fs.stat(uri, (err, stat) => {
if (!err) return serveFile(uri, req, res, stat, resource, status, fallback, runtime);
if (!isRoute) return sendError(res, resource, 404);
// Check directory
resource = decodeURI(url.pathname);
uri = join(root, resource);
fs.stat(uri, (err, stat) => {
if (!err) return showDir(uri, res, resource);
// Try to send fallback /index.html
resource = `/${fallback}`;
uri = join(root, resource);
status = 301;
serveFile(uri, req, res, stat, resource, status, fallback, runtime);
});
});
}).listen(parseInt(port, 10));
// Output server details to the console
console.log(
`\n🗂 Serving:\t${root}\n🏡 Local:\thttp://localhost:${port}\n${ips
.map((ip) => `📡 Network:\thttp://${ip}:${port}`)
.join('\n ')}\n`
);
}
// Find available port
function fport() {
return new Promise((res) => {
const s = net.createServer().listen(0, () => {
const { port } = s.address();
s.close(() => res(port));
});
});
}
function isRouteRequest(pathname) {
return !~pathname.split('/').pop().indexOf('.');
}
function showDir(uri, res, resource) {
fs.readdir(uri, (err, files) => {
if (err) return sendError(res, resource, 500);
res.setHeader('Content-Type', 'text/html');
let html = `
<html>
<head>
<meta name="viewport" content="width=device-width">
<title>Index of ${resource}</title>
</head>
<body>
<h1>Index of ${resource}</h1>
<ul>${files.map((path) => `<li><a href="${resource}${path}">${path}</a></li>`).join('')}</ul>
</body>
</html>
`;
res.end(html);
});
}
// Server utility functions
function sendError(res, resource, status) {
res.writeHead(status);
res.end();
debug && console.log(' \x1b[41m', status, '\x1b[0m', `${resource}`);
}
function serveFile(uri, req, res, stat, resource, status, fallback, runtime) {
const ext = uri.replace(/^.*[\.\/\\]/, '').toLowerCase();
res.setHeader('Content-Type', mime[ext] || 'application/octet-stream');
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.headers.range) {
status = 206;
streamFile(uri, req, res, stat, resource, status);
} else {
fs.readFile(uri, 'binary', (err, file) => {
if (err) return sendError(res, resource, 404);
if (runtime && ~resource.indexOf(`/${fallback}`)) runtime(resource.split(fallback)[0]);
sendFile(res, stat, resource, status, file);
});
}
}
function sendFile(res, stat, resource, status, file) {
res.writeHead(status, { 'Content-Length': stat.size });
res.write(file, 'binary');
res.end();
debug && console.log(' \x1b[42m', status, '\x1b[0m', `${resource}`);
}
function streamFile(uri, req, res, stat, resource, status) {
const total = stat.size;
const parts = req.headers.range
.trim()
.replace(/bytes=/, '')
.split('-');
const partialstart = parts[0];
const partialend = parts[1];
const start = parseInt(partialstart, 10);
const end = Math.min(total - 1, partialend ? parseInt(partialend, 10) : total - 1);
const chunksize = end - start + 1;
if (start > end || isNaN(start) || isNaN(end)) return sendError(res, resource, 416);
res.writeHead(status, {
'Content-Range': `bytes ${start}-${end}/${total}`,
'Content-Length': chunksize,
'Accept-Ranges': 'bytes',
});
fs.createReadStream(uri, { start, end })
.on('error', () => sendError(res, resource, 500))
.pipe(res);
}
(async () => {
await serve({
root,
port,
fallback: 'index.html',
// When serving index.html, trigger project runtime script if exists
runtime: (path) => {
const runtimePath = `${root}${path}/runtime/runtime.js`;
if (!fs.existsSync(runtimePath)) return;
try {
execSync(`node ${runtimePath}`, { stdio: 'inherit' });
} catch (e) {}
},
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment