Created
May 19, 2020 07:55
-
-
Save gordonnl/795c0ef00d09f7f31134851f009ac809 to your computer and use it in GitHub Desktop.
Node development server
This file contains 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
// 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