reboost
Version:
A super fast dev server for rapid web development
256 lines (243 loc) • 10 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createContentServer = void 0;
const tslib_1 = require("tslib");
const koa_1 = (0, tslib_1.__importDefault)(require("koa"));
const koa_proxies_1 = (0, tslib_1.__importDefault)(require("koa-proxies"));
const koa_send_1 = (0, tslib_1.__importDefault)(require("koa-send"));
const chalk_1 = (0, tslib_1.__importDefault)(require("chalk"));
const chokidar_1 = require("chokidar");
const ws_1 = (0, tslib_1.__importDefault)(require("ws"));
const node_html_parser_1 = require("node-html-parser");
const fs_1 = (0, tslib_1.__importDefault)(require("fs"));
const path_1 = (0, tslib_1.__importDefault)(require("path"));
const utils_1 = require("./utils");
const createDirectoryServer = (options) => {
const styles = /* css */ `
* {
font-family: monospace;
--link: rgb(0, 0, 238);
}
body {
padding: 20px;
}
h2 {
font-weight: normal;
}
ul {
padding-inline-start: 20px;
}
li {
list-style: none;
}
li a {
padding: 5px 0px;
text-decoration: none;
font-size: 1.2rem;
color: var(--link);
border-bottom-style: solid;
border-width: 2px;
border-color: transparent;
transition: 0.05s;
display: flex;
align-items: center;
}
li a:hover {
border-color: var(--link);
}
li a:visited {
color: var(--link);
}
[icon] {
--size: 1.5rem;
height: var(--size);
width: var(--size);
display: inline-block;
margin-right: 0.5rem;
}
/* Icons are from https://materialdesignicons.com/ */
[icon=directory] {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' style='width:24px;height:24px' viewBox='0 0 24 24'%3E%3Cpath fill='currentColor' d='M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z' /%3E%3C/svg%3E");
}
[icon=file] {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' style='width:24px;height:24px' viewBox='0 0 24 24'%3E%3Cpath fill='currentColor' d='M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z' /%3E%3C/svg%3E");
}
[icon=go-up] {
transform: rotate(90deg);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' style='width:24px;height:24px' viewBox='0 0 24 24'%3E%3Cpath fill='currentColor' d='M11,9L12.42,10.42L8.83,14H18V4H20V16H8.83L12.42,19.58L11,21L5,15L11,9Z' /%3E%3C/svg%3E");
}
`;
const basePathLength = options.basePath.length;
return (ctx, root) => {
const requestedPath = ctx.path.substring(basePathLength);
const dirPath = path_1.default.join(root, requestedPath);
if (!fs_1.default.existsSync(dirPath) || !(0, utils_1.isDirectory)(dirPath))
return;
const all = fs_1.default.readdirSync(dirPath);
const directories = all.filter((file) => (0, utils_1.isDirectory)(path_1.default.join(dirPath, file))).sort();
const files = all.filter((file) => !directories.includes(file)).sort();
/* eslint-disable indent */
ctx.type = 'text/html';
ctx.body = /* html */ `
<!doctype html>
<html>
<head>
<title>Index of ${ctx.path}</title>
<style>${styles}</style>
</head>
<body>
<h2>Index of ${ctx.path}</h2>
<ul>
${requestedPath && requestedPath !== '/' ? /* html */ `
<li>
<a href="../">
<i icon="go-up"></i>
Go up
</a>
</li>
` : ''}
${directories.concat(files).map((file) => {
const isDir = directories.includes(file);
const full = file + (isDir ? '/' : '');
return /* html */ `
<li>
<a href="./${full}">
<i icon="${isDir ? 'directory' : 'file'}"></i>
${full}
</a>
</li>
`;
}).join('\n')}
</ul>
</body>
</html>
`;
/* eslint-enable indent */
};
};
const attachFileServer = (instance, app, options) => {
const sendDirectory = createDirectoryServer(options);
const { debugMode } = instance.config;
const { root } = options;
const sendOptions = {
root,
extensions: options.extensions,
hidden: options.hidden,
index: options.index
};
const loadInitCode = () => fs_1.default.readFileSync(path_1.default.join(__dirname, '../browser/content-server.js'), 'utf8');
const initCode = loadInitCode();
const initScriptPath = `/reboost-${(0, utils_1.uniqueID)(10)}`;
const webSockets = new Set();
const watcher = new chokidar_1.FSWatcher();
const watchedFiles = new Set();
instance.onStop("Closes content server's file watcher", () => watcher.close());
const triggerReload = (isCSS = false) => {
webSockets.forEach((ws) => ws.send(JSON.stringify(isCSS)));
};
const rootRelative = (filePath) => path_1.default.relative(instance.config.rootDir, filePath);
watcher.on('change', (filePath) => {
instance.log('info', chalk_1.default.blue(`${(0, utils_1.getTimestamp)()} Changed: ${rootRelative(filePath)}`));
triggerReload(path_1.default.extname(filePath) === '.css');
});
watcher.on('unlink', (filePath) => {
instance.log('info', chalk_1.default.blue(`${(0, utils_1.getTimestamp)()} Deleted: ${rootRelative(filePath)}`));
watchedFiles.delete(path_1.default.normalize(filePath));
triggerReload();
});
(0, utils_1.onServerCreated)(app, (server) => {
const wss = new ws_1.default.Server({ server });
wss.on('connection', (socket) => {
webSockets.add(socket);
socket.on('close', () => webSockets.delete(socket));
});
instance.onStop("Closes content server's websocket", () => wss.close());
});
const initScriptHTML = `<script src="${initScriptPath}"></script>`;
const etagKey = (0, utils_1.uniqueID)(10) + '-';
app.use(async (ctx, next) => {
if (ctx.path === initScriptPath) {
ctx.type = 'text/javascript';
ctx.body = `const debugMode = ${instance.config.debugMode};\n\n`;
ctx.body += debugMode ? loadInitCode() : initCode;
return next();
}
if (!ctx.path.startsWith(options.basePath))
return next();
const requestedPath = ctx.path.substring(options.basePath.length);
if (!requestedPath && !ctx.path.endsWith('/')) {
ctx.redirect(ctx.path + '/');
return;
}
let sentFilePath;
try {
sentFilePath = await (0, koa_send_1.default)(ctx, requestedPath || '/', sendOptions);
sentFilePath = path_1.default.normalize(sentFilePath);
}
catch (err) { /* Ignored */ }
if (sentFilePath) {
if (!watchedFiles.has(sentFilePath)) {
watcher.add(sentFilePath);
watchedFiles.add(sentFilePath);
}
if (options.etag) {
const etag = etagKey + Math.floor(fs_1.default.statSync(sentFilePath).mtimeMs);
if (ctx.get('If-None-Match') === etag) {
ctx.status = 304;
ctx.body = undefined;
ctx.remove('Content-Length');
return next();
}
else {
ctx.set('ETag', etag);
}
}
if (/^\.html?$/.test(path_1.default.extname(sentFilePath))) {
const htmlSource = await new Promise((res) => {
const stream = ctx.body;
const chunks = [];
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
stream.on('end', () => res(Buffer.concat(chunks).toString()));
});
let responseHTML;
if (htmlSource.trim() === '') {
responseHTML = `<html><body>${initScriptHTML}</body></html>`;
}
else {
const htmlRoot = (0, node_html_parser_1.parse)(htmlSource, { comment: true });
const body = htmlRoot.querySelector('body');
if (body) {
body.appendChild((0, node_html_parser_1.parse)(initScriptHTML));
}
responseHTML = htmlRoot.toString();
}
ctx.body = responseHTML;
ctx.type = 'text/html';
ctx.remove('Content-Length');
}
return next();
}
if (options.serveIndex)
sendDirectory(ctx, root);
return next();
});
};
const createContentServer = (instance, options) => {
const contentServer = new koa_1.default();
const { middleware } = options;
if (middleware) {
[].concat(middleware).forEach((fn) => contentServer.use(fn));
}
const proxyObject = options.proxy;
if (proxyObject) {
for (const key in proxyObject) {
const proxyOptions = typeof proxyObject[key] === 'string'
? { target: proxyObject[key] }
: proxyObject[key];
contentServer.use((0, koa_proxies_1.default)(key, proxyOptions));
}
}
attachFileServer(instance, contentServer, options);
return contentServer;
};
exports.createContentServer = createContentServer;