dhost
Version:
Never-caching development Node webserver
125 lines (105 loc) • 4.09 kB
JavaScript
import buildHandler from '../lib/handler.js';
import bytes from 'bytes';
import * as color from 'colorette';
import * as network from './network.js';
import * as path from 'path';
import * as types from '../types/index.js';
import { copyToClipboard } from './clipboard.js';
import { bindAndStart } from '../lib/server.js';
/**
* @param {types.MainOptions} options
* @return {Promise<never>}
*/
export async function main(options = {}) {
const handler = buildHandler(options);
// This isn't really any of the types objects, but is close enough.
const o = Object.assign({
path: '.',
}, options);
const server = await bindAndStart(o, (req, res) => {
handler(req, res, () => {
let status = 404;
if (req.method !== 'GET' && req.method !== 'HEAD') {
status = 405;
}
res.writeHead(status);
res.end();
}).catch((err) => {
console.info(color.red('!'), err);
res.writeHead(500);
res.end();
});
});
const serverAddress = server.address();
if (!serverAddress || !(typeof serverAddress === 'object')) {
throw new Error(`could not find serverAddress: ${serverAddress}`);
}
const localURL = `http://localhost:${serverAddress.port}`;
let clipboardError = null;
try {
// Copying to clipboard can fail on headless Linux systems (possibly others).
copyToClipboard(localURL);
} catch (e) {
clipboardError = e;
}
console.info(color.blue('*'), 'Serving static files from', color.cyan(path.resolve(o.path)));
console.info(color.blue('*'), 'Local', color.green(localURL), clipboardError ? color.red('(could not copy to clipboard)') : color.dim('(on your clipboard!)'));
if (o.bindAll) {
// log all IP addresses we're listening on
const ips = network.localAddresses();
ips.forEach(({address, family}) => {
let display = address;
if (family === 'IPv6') {
display = `[${display}]`;
}
console.info(color.blue('*'), 'Network', color.green(`http://${display}:${serverAddress.port}`));
});
}
const padSize = Math.min(80, localURL.length * 2);
console.info(''.padEnd(padSize, '-'));
server.on('request', (req, res) => {
const requestParts = [req.url];
const forwardedFor = req.headers['x-forwarded-for'] || '';
const remoteAddresses = network.mergeForwardedFor(forwardedFor, req.socket.remoteAddress);
for (const remoteAddress of remoteAddresses) {
requestParts.push(color.gray(remoteAddress)); // display remote addr for non-localhost
}
console.info(color.gray('>'), color.cyan(req.method), requestParts.join(' '));
const start = process.hrtime();
res.on('finish', () => {
const duration = process.hrtime(start);
const ms = ((duration[0] + (duration[1] / 1e9)) * 1e3).toFixed(3);
const responseColor = res.statusCode >= 400 ? color.red : color.green;
const responseParts = [responseColor(res.statusCode), responseColor(res.statusMessage)];
// Render the served URL, or the 3xx 'Location' field
let url = req.url;
if (res.statusCode >= 300 && res.statusCode < 400) {
const location = res.getHeader('Location');
if (location) {
if (path.isAbsolute(location)) {
url = location;
} else {
let base = req.url;
if (base.endsWith('/')) {
base += '.';
}
url = path.join(path.dirname(base), location);
}
}
}
responseParts.push(url);
responseParts.push(color.dim(`${ms}ms`));
// Content-Length is only set by user code, not by Node, so it might not always exist
const contentLength = res.getHeader('Content-Length');
if (contentLength != null) { // null or undefined
const displayBytes = bytes(+contentLength || 0);
responseParts.push(displayBytes);
}
console.info(color.gray('<'), responseParts.join(' '));
});
});
await new Promise((_, reject) => {
server.on('error', reject);
});
throw new Error(`internal error, should never shutdown`);
}