ohif-viewer/platform/viewer/server.js
Danny Brown 49d343941e
fix: measurementsAPI issue caused by production build (#842)
* ci: test docs-publish

* Specify to use prod

* Babel should transpile with env set by webpack

* in-progress

* in-progress

* Polyfill for ie11 and edge features

* Ditch polyfills w/ babel - we'll use a service for now

* Bump tools version; shift vtk.js up a layer

* Specify we shouldn't target older than IE 11

* ditch babel plugins that should be covered by preset-env

* Add a top level build demo command

* Let our babel config determine settings

* Same babel fixes as PWA

* Rebuild deps that don't satisfy our target

* Mini helper script for excluding all node_modules, except...

* Shift vtk.js dep up a layer

* Kill core-js

* Export in a node happy way

* Updated yarn lock

* Set NODE_ENV when launching anything w/ WebPack

* docs: updated FAQ

* docs: on browser support

* Add support for redux browser extension

* misc. small clean-up

* docs: Remove roadmap page; add browser-support to sidebar

* Formatting

* Remove roadmap links

* Formatting

* ci: Remove config syntax error

* Simplified bug report template

* update community request template

* Update question's template

* simplify build scripts

* specify new script names

* fix: for measurement api being pruned by minimizer in prod builds

* Use named exports

* Simplify config

* Let's not do so much heavy lifting for a dev-server build

* fix dev build

* Add hotkeys to demo

* fix: jest babel config and env specific configs

* Remove call to non-existant command

* Shift experimental proposal plugin up a layer

* Use `https`

* Try with reduced number of package exceptions

* Try to resolve cypress issue

* Try to fix cypress issue in CI

* Skip https
2019-09-04 13:53:13 -04:00

1025 lines
26 KiB
JavaScript

/**
* @author Szymon Działowski
* @date 29 Nov 2017
* @license MIT
* @homepage https://github.com/stopsopa/npx-server
*/
var http = require('http');
var path = require('path');
var fs = require('fs');
const spawn = require('child_process').spawn;
var thisScript = path.basename(__filename);
var script = 'node ' + thisScript;
var sp = ['node', [thisScript]];
var staticServer = false;
// var staticServer = false;
let staticServerAbs = false;
if (staticServer) {
staticServerAbs = path.resolve(__dirname, staticServer);
}
process.on('SIGINT', function (signal) {
console.log(`signal SIGINT..., signal: ${signal}`)
process.exit(0);
});
process.on('SIGTERM', function (signal) {
console.log(`signal SIGTERM..., signal: ${signal}`)
process.exit(0);
});
const log = function () {
Array.prototype.slice.call(arguments).map(i => i + "\n").forEach(i => process.stdout.write(i));
};
function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
};
function isObject(a) {
return Object.prototype.toString.call(a) === '[object Object]';
};
var mkdirP = (function () {
const module = {};
/**
* https://github.com/substack/node-mkdirp/blob/master/index.js
*/
var path = require('path');
var fs = require('fs');
var _0777 = parseInt('0777', 8);
module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP;
function mkdirP (p, opts, f, made) {
if (typeof opts === 'function') {
f = opts;
opts = {};
}
else if (!opts || typeof opts !== 'object') {
opts = { mode: opts };
}
var mode = opts.mode;
var xfs = opts.fs || fs;
if (mode === undefined) {
mode = _0777 & (~process.umask());
}
if (!made) made = null;
var cb = f || function () {};
p = path.resolve(p);
xfs.mkdir(p, mode, function (er) {
if (!er) {
made = made || p;
return cb(null, made);
}
switch (er.code) {
case 'ENOENT':
mkdirP(path.dirname(p), opts, function (er, made) {
if (er) cb(er, made);
else mkdirP(p, opts, cb, made);
});
break;
// In the case of any other error, just see if there's a dir
// there already. If so, then hooray! If not, then something
// is borked.
default:
xfs.stat(p, function (er2, stat) {
// if the stat fails, then that's super weird.
// let the original error be the failure reason.
if (er2 || !stat.isDirectory()) cb(er, made)
else cb(null, made);
});
break;
}
});
}
mkdirP.sync = function sync (p, opts, made) {
if (!opts || typeof opts !== 'object') {
opts = { mode: opts };
}
var mode = opts.mode;
var xfs = opts.fs || fs;
if (mode === undefined) {
mode = _0777 & (~process.umask());
}
if (!made) made = null;
p = path.resolve(p);
try {
xfs.mkdirSync(p, mode);
made = made || p;
}
catch (err0) {
switch (err0.code) {
case 'ENOENT' :
made = sync(path.dirname(p), opts, made);
sync(p, opts, made);
break;
// In the case of any other error, just see if there's a dir
// there already. If so, then hooray! If not, then something
// is borked.
default:
var stat;
try {
stat = xfs.statSync(p);
}
catch (err1) {
throw err0;
}
if (!stat.isDirectory()) throw err0;
break;
}
}
return made;
};
return module.exports;
}())
const prepareDir = target => {
try {
mkdirP.sync(target);
}
catch (e) {
process.stdout.write(`\n ERROR prepareDir: ${e}\n`);
process.exit(1);
}
};
const raceThrottleDebounce = (function () {
var crypto = require('crypto');
return function (fn) {
var last = false;
var triggeredFor = false;
var args;
var newFn = function () {
last = (new Date()).getTime() + crypto.randomBytes(5).toString('hex');
args = Array.prototype.slice.call(arguments);
if (triggeredFor) {
return ;
}
triggeredFor = last;
return fn.apply(this, args);
};
newFn.unlock = function () {
if (triggeredFor === last) {
return triggeredFor = last = false;
}
triggeredFor = false;
newFn.apply(this, args);
};
return newFn;
}
}());
const args = (function (obj, tmp) {
process.argv
.slice(2)
.map(a => {
if (a.indexOf('--') === 0) {
tmp = a.substring(2).replace(/^\s*(\S*(\s+\S+)*)\s*$/, '$1');
if (tmp) {
obj[tmp] = (typeof obj[tmp] === 'undefined') ? true : obj[tmp];
}
return;
}
if (a === 'true') {
a = true
}
if (a === 'false') {
a = false
}
if (tmp !== null) {
if (obj[tmp] === true) {
return obj[tmp] = [a];
}
try {
obj[tmp].push(a);
}
catch (e) {
}
}
})
;
Object.keys(obj).map(k => {
(obj[k] !== true && obj[k].length === 1) && (obj[k] = obj[k][0]);
(obj[k] === 'false') && (obj[k] = false);
});
return {
all: () => JSON.parse(JSON.stringify(obj)),
get: (key, def) => {
var t = JSON.parse(JSON.stringify(obj));
if (typeof def === 'undefined')
return t[key];
return (typeof t[key] === 'undefined') ? def : t[key] ;
},
update: data => {
delete data['config'];
delete data['dump'];
delete data['help'];
delete data['inject'];
obj = data;
}
};
}({}));
// const config = {/* */}
// const log = console.log;
// // // https://nodejs.org/docs/latest/api/all.html#modules_accessing_the_main_module
// if (require.main === module) {
// function isObject(a) {
// return ['[object Object]',"[object Array]"].indexOf(Object.prototype.toString.call(a)) > -1;
// };
// const a = process.argv.slice(2);
// if (a.indexOf('--param') > -1) {
// const key = ( a[1] || '' ).split('.');
// let k, tmp = config;
// while (k = key.shift()) {
// tmp = tmp[k];
// }
// if (isObject(tmp)) {
// process.stdout.write(JSON.stringify(tmp, null, ' '));
// }
// else {
// process.stdout.write(tmp + '');
// }
// }
// }
// else {
// module.exports = config;
// }
if (args.get('help')) {
let ssman = '';
if (staticServer) {
ssman = `
--gc "/path"
generate controller /path in directory '${staticServerAbs}'
`;
}
process.stdout.write(`
Standalone static files http server with no dependencies
@homepage https://github.com/stopsopa/npx-server
@date 29 Nov 2017
@license MIT
@author Szymon Działowski https://github.com/stopsopa
parameters:
--port [port] def: 8080
--dir [path] def: '.'
relative or absolute path to directory with files to serve
--config [filepath.json] def: false
path to config file (json format), file can containe object where
kays are parameters of this script ('config' param in file will be ignored)
--noindex def: false
disable indexing directories content if url points to directory
--log [level] def: 1
binary mask:
0 - show nothing
1 - show 404,
2 - show 200,
4 - show 301
8 - autoindex
more examples:
3 - show 404 and 200
6 - show 200 and 301
7 - show all without autoindex
15 - show all
--watch [regex] def: false
reload currently opened page in browser when files will change
examples:
node ${thisScript} --watch - watch all files (can be slow)
node ${thisScript} --watch '/\\.js$/i' - reload only when files with 'js' extension will change
node ${thisScript} --watch '\\.js$' - like above but shorter syntax (if no regex flags)
node ${thisScript} --watch '/\\.js$/i' --watch '/\\.html$/i' - reload for 'js' and 'html' files
node ${thisScript} --watch '/\\.(js|html)$/i' - like above but in one regex
--ignore [regex] def: '/^(.*?\\/)?\\.[^\\/]+$/g' (all files starting from ".")
ignore watching files (this param takes precedense over --watch param)
--debug [true|false] def: false
flag for debugging --watch and --ignore parameters behaviour
--dump
output config
${ssman}
--flag
just extra allowed flag for searching processes using 'ps aux | grep [flagvalue]'
`);
process.exit(0);
}
var gc = args.get('gc');
if (staticServer && gc) {
let file = gc.replace(/[^a-z\d-_]/g, '') + '.js';
if ( ! file ) {
process.stdout.write(`Can't generate path from url '${gc}'`);
process.exit(1);
}
let checkPath = gc;
if (gc.indexOf('/') === 0) {
checkPath = checkPath.substring(1);
}
checkPath = path.resolve(__dirname, checkPath);
if (fs.existsSync(checkPath)) {
process.stdout.write(`Error:\n Can't create controller '${checkPath}' path is already taken`);
process.exit(1);
}
file = path.resolve(staticServerAbs, file);
prepareDir(path.dirname(file));
if (fs.existsSync(file)) {
process.stdout.write(`Error:\n Controller '${file}' already exist`);
process.exit(1);
}
fs.appendFileSync(file, `
// https://nodejs.org/api/http.html#http_class_http_serverresponse
const controller = (req, res, query = {}, json = {}) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
// res.setHeader('Content-Type', 'text/plain; charset=utf-8');
// res.statusCode = 404;
res.end(JSON.stringify({
page: {
query,
json,
},
node: process.version,
}));
}
controller.url = '${gc}';
module.exports = controller;
`);
if ( fs.existsSync(file) ) {
process.stdout.write(`Controller '${file}' successfully created`);
}
else {
process.stdout.write(`Error:\n Controller '${file}' couldn't be created`);
process.exit(1);
}
process.exit(0);
}
var config = args.get('config');
var dump = args.get('dump');
if (config) {
var configData = require(path.resolve(__dirname, config));
if ( ! isObject(configData) ) {
throw `data from config file '${config}' should be an object`;
}
args.update(Object.assign(configData, args.all()));
}
args.update(args.all());
if (dump) {
log(JSON.stringify(args.all(), null, ' '));
}
const diff = function(a, b) {
return a.filter(function(i) {return b.indexOf(i) < 0});
};
(function (d) {
if (d.length) {
log(`Unknown parameters: ${d.join(', ')}\ncheck \n\n node ${thisScript} --help\n\nfor more help`);
process.exit(1);
}
}(diff(Object.keys(args.all()), ('port dir noindex log help watch ignore inject debug config dump flag' + (staticServer ? ' gc' : '')).split(' '))));
function execArgs (args, str) {
var arr = ['--inject'];
Object.keys(args).forEach(key => {
if (['watch', 'ignore'].indexOf(key) > -1) {
return;
}
if (isArray(args[key])) {
args[key].forEach(val => {
arr.push('--' + key)
arr.push(val)
});
}
else {
if (args[key] === true) {
arr.push('--' + key)
}
else {
arr.push('--' + key)
arr.push(args[key])
}
}
});
if (str) {
return arr.map(a => '"' + (a + '').replace(/"/g, '\\"') + '"').join(' ')
}
return arr;
}
var dir = path.resolve(__dirname, args.get('dir', '.'));
var regexps = (function () {
function split(reg) {
var tmp = reg.substring(1).match(/^(.*)\/([^\/]*)$/);
if (tmp) {
return tmp.slice(1);
}
return [reg];
}
return function (list, debug) {
return list.filter(i => (typeof i === 'string')).filter(i => i.replace(/^\s*(\S*(\s+\S+)*)\s*$/, '$1')).map(i => {
if (i[0] === '/') {
i = split(i);
return new RegExp(i[0], i[1]);
}
return new RegExp(i);
});
}
}());
var debug = args.get('debug');
var ignore = args.get('ignore', '/^\\./');
if (ignore === true) {
ignore = false;
}
else {
if ( ! isArray(ignore)) {
ignore = [ignore];
}
ignore = regexps(ignore).filter(i => i);
if (ignore.length === 0) {
ignore = false;
}
}
var watch = args.get('watch', false);
if (typeof watch !== 'boolean') {
if ( ! isArray(watch) ) {
watch = [watch];
}
watch = regexps(watch);
if (watch.length === 0) {
watch = true;
}
}
script += ' ' + execArgs(args.all(), true);
sp[1] = sp[1].concat(execArgs(args.all()));
if (watch) {
var child;
var event = raceThrottleDebounce(function (eventType, filename) {
debug && log('try: ' + filename);
if (filename) {
if (isArray(ignore)) {
for (var i = 0, l = ignore.length ; i < l ; i += 1 ) {
debug && log(`before test (ignore): /${ignore[i].source}/${ignore[i].flags} against: `+ filename);
if (ignore[i].test(filename)) {
debug && log(`ignored: /${ignore[i].source}/${ignore[i].flags} `+ filename);
return event.unlock();
}
}
}
if (isArray(watch)) {
for (var matched = false, i = 0, l = watch.length ; i < l ; i += 1 ) {
debug && log(`before test (watch): /${watch[i].source}/${watch[i].flags} against: `+ filename);
if (watch[i].test(filename)) {
debug && log(`matched: /${watch[i].source}/${watch[i].flags} `+ filename);
matched = true;
break;
}
}
if ( ! matched ) {
return event.unlock();
}
}
}
if (child) {
log(eventType + ': ' + filename + ', restarting process: ' + script);
child.kill();
}
child = spawn(sp[0], sp[1]);
child.stdout.on('data', (data) => process.stdout.write(data));
event.unlock();
});
event();
fs.watch(dir, {
recursive: true
}, event);
}
else {
var port = parseInt(args.get('port', 8080), 10);
var logs = parseInt(args.get('log', 1), 10);
var inject = args.get('inject');
var type = (function (types) {
return function (req, res, ext) {
ext = ext || path.extname(req.url.toLowerCase().split('?')[0]).replace(/[^a-z0-9]/g, '');
types[ext] && res.setHeader('Content-Type', types[ext]);
return ext;
}
}((function (type) {
type.jpeg = type.jpg;
type.log = type.txt;
return type;
}({
html : 'text/html; charset=utf-8',
js : 'application/javascript; charset=utf-8',
css : 'text/css; charset=utf-8',
json : 'application/json; charset=utf-8',
txt : 'text/plain; charset=utf-8',
gif : 'image/gif',
bmp : 'image/bmp',
jpg : 'image/jpeg',
png : 'image/png',
pdf : 'application/pdf',
ico : 'image/x-icon',
}))));
function time() {
return (new Date()).toISOString().substring(0, 19).replace('T', ' ');
}
var server = http.createServer().listen(port);
function noAccess(req, res, isDir, notype) {
res.statusCode = 403;
type(req, res, 'html');
(logs & 2) && log(`${time()} \x1b[35m${res.statusCode}\x1b[0m: ${req.url}`);
res.end(`<div style="color: #92317B; font-family: tahoma;">${notype ? '' : (isDir ? 'directory' : 'file')} ${req.url} no access.</div>`);
}
var uniq = (function unique(pattern) {
return pattern.replace(/[xy]/g,
function(c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}('xyxyxyxyxyxyx'));
function addWatcher(content, ext) {
if (inject && ext === 'html') {
content = content.replace(
/(<\s*\/\s*body\s*>)/i,
`<script>(function run(uniq){fetch('?watch='+uniq).then(res=>res.text()).then(hash=>run(hash),()=>(function test(){return fetch(location.href).then(()=>location.href=location.href,()=>setTimeout(test,500))}()))}('${uniq}'))</script>$1`
);
}
return content;
}
const controllers = () => {
const dir = path.resolve(__dirname, staticServer);
if ( ! fs.existsSync(dir) ) {
return [];
}
return fs.readdirSync(dir)
.reduce((acc, file) => {
const lib = path.resolve(dir, file);
try {
const ext = path.extname(file);
if (ext !== '.js') {
return acc;
};
delete require.cache[lib];
const module = require(lib);
module.__file = lib;
acc.push(module);
}
catch (e) {
log(`controllers error [${lib}]: `, e)
}
return acc;
}, [])
;
}
// base64 online encoder: https://xaviesteve.com//pro/base64.php
const favicon = new Buffer('iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAFdQTFRF/////v7+7e3t4+Pj4uLi7u7urKysSEhINjY2SUlJtLS0GhoaMzMzysrK5OTkODg4S0tLHR0duLi4MjIySkpK5eXl7+/vHBwc8/PzbGxsNTU18fHx7u/vzVzHUQAAAAFiS0dEAIgFHUgAAAAJcEhZcwAAFiUAABYlAUlSJPAAAADdSURBVDjLnZPZEoMgDEWBQAVF0Nq9/f/vbBAXNnXa++KMOQPJMRIShi4h8XsGLlxQj5AUOFVyjKpJMaxqtKvrRrEiAFK3BsC0WsIG4Avz8x/g4AqhsElrsclO5FU3ca2k7nstu3Oh7JxQhsfjNYKWgKjRySks6GJ1AjKnCSAypwGAPZh2SAZePxxYnAJnsbGyX4DDK3yTl7nJySlNPaDTMbPTDKDCXDHmFojaBbITCiajpRHb27kxZgjcS6LWPJ4vuQuQN7ijh4Pt/KQms6Qms/WljHPuf+O8XvjtMV9rxg4RtlXOXgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOC0xMS0yN1QxMDowMzowMSswMDowMIg1+6IAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTgtMTEtMjdUMTA6MDM6MDErMDA6MDD5aEMeAAAARnRFWHRzb2Z0d2FyZQBJbWFnZU1hZ2ljayA2LjcuOC05IDIwMTQtMDUtMTIgUTE2IGh0dHA6Ly93d3cuaW1hZ2VtYWdpY2sub3Jn3IbtAAAAABh0RVh0VGh1bWI6OkRvY3VtZW50OjpQYWdlcwAxp/+7LwAAABh0RVh0VGh1bWI6OkltYWdlOjpoZWlnaHQAMTkyDwByhQAAABd0RVh0VGh1bWI6OkltYWdlOjpXaWR0aAAxOTLTrCEIAAAAGXRFWHRUaHVtYjo6TWltZXR5cGUAaW1hZ2UvcG5nP7JWTgAAABd0RVh0VGh1bWI6Ok1UaW1lADE1NDMzMTI5ODEv0kVFAAAAD3RFWHRUaHVtYjo6U2l6ZQAwQkKUoj7sAAAAVnRFWHRUaHVtYjo6VVJJAGZpbGU6Ly8vbW50bG9nL2Zhdmljb25zLzIwMTgtMTEtMjcvNzMzODU0Y2FjM2I1MGI4NGE3NmIwODY1MmY4NDdjYjQuaWNvLnBuZwnzw3IAAAAASUVORK5CYII=', 'base64');
server.on('request', function (req, res) {
var url = req.url.split('?')[0];
if (req.url === '/favicon.ico') {
(logs & 4) && log(`${time()} \x1b[33m${res.statusCode}\x1b[0m: ${url}`);
res.statusCode = 200;
res.setHeader('Content-Type', 'image/x-icon');
res.setHeader("Cache-Control", "public, max-age=2592000"); // expiers after a month
res.setHeader("Expires", new Date(Date.now() + 2592000000).toUTCString());
return res.end(favicon);
}
if (req.url === '/run-sandbox-server.sh-check') {
res.statusCode = 201;
return res.end('ok')
}
if (inject) {
var test = req.url.match(/\?watch=(.*)/);
if (test) {
if (test[1] !== uniq) {
return res.end(uniq)
}
return;
}
}
// (function (a) {
// if (url.indexOf(a) === 0) {
// url = url.substring(a.length);
// }
// }('/app_dev.php'));
var query = req.url.split('?')[1];
if (typeof query === 'string') {
query = query.split('&').reduce((acc, val) => {
var
a = val.split(/=/),
key = a.shift(),
dec = a.join('=')
;
acc[key] = decodeURIComponent(dec);
return acc;
}, {});
if (typeof query._redirect === 'string') {
res.writeHead(query._status || 301, { 'Location': query._redirect });
return setTimeout(() => res.end(), query._timeout ? (parseInt(query._timeout, 10) || 0) : 0);
}
}
var file = path.resolve(dir, '.' + path.sep + (decodeURI(url).replace(/\.\.+/g, '.')));
if (fs.existsSync(file)) {
var isDir = fs.statSync(file).isDirectory();
if (isDir) {
if (args.get('noindex')) {
return noAccess(req, res, isDir, true);
}
if (url.length > 1 && url.substr(-1) !== '/') {
res.writeHead(302, { 'Location': url + '/' });
(logs & 4) && log(`${time()} \x1b[33m${res.statusCode}\x1b[0m: ${url} -> ${url + '/'}`);
return res.end();
}
try {
var list = `
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
</head>
<body>
<style>
*{font-family:tahoma;font-size:12px}
ul{padding:0;list-style-type:none}
a{padding-right:20px;padding-left:3px;margin-left:3px;border-left:1px solid transparent}
a:hover{border-left:1px solid gray}
</style>
<ul><li>📁<a href=".."> .. </a></li>
</body>
</html>
`;
list += fs.readdirSync(file).map(f => {
var dir = fs.statSync(path.resolve(file, f)).isDirectory();
return `<li>${dir?'📁':'📄'}<a href="./${f}${dir?'/':''}">${f}</a></li>`;
}).join("\n");
list += '</ul>';
}
catch (e) {
return noAccess(req, res, isDir);
}
(logs & 8) && log(`${time()} \x1b[36m${res.statusCode}\x1b[0m: [\x1b[36mautoindex\x1b[0m] ${req.url}`);
return res.end(addWatcher(list, type(req, res, 'html')));
}
try {
res.end(addWatcher(fs.readFileSync(file), type(req, res)));
}
catch (e) {
return noAccess(req, res, isDir);
}
(logs & 2) && log(`${time()} \x1b[32m${res.statusCode}\x1b[0m: ${req.url}`);
}
else {
let found = false;
if (staticServer) {
found = controllers().find(c => c.url === url);
}
if (found) {
let json='';
req.setEncoding('utf8');
req.on('data', function(chunk) {
json += chunk;
});
req.on('end', function() {
try {
json = JSON.parse(json);
} catch (e) {
json = e;
}
try {
found(req, res, query, json);
}
catch (e) {
res.setHeader(`Content-Type`, `application/json; charset=utf-8`);
res.statusCode = 500;
res.end(JSON.stringify({
exception: 'General controller exception',
controller: found.__file,
exceptionMessage: e.message,
exceptionMessageSplit: (e.message + '').split("\n")
}));
}
});
(logs & 1) && log(`${time()} \x1b[93m${res.statusCode}\x1b[0m: ${req.url}`);
}
else {
res.statusCode = 404;
res.end(`<div style="color: #b10000; font-family: tahoma;">status code ${res.statusCode}: ${req.url}</div>`);
(logs & 1) && log(`${time()} \x1b[31m${res.statusCode}\x1b[0m: ${req.url}`);
}
}
});
log(`
🌎 Listening on port ${port}, start time: ${time()}
serving files from directory ${dir}, --help for more info
`);
}