UNPKG

reboost

Version:

A super fast dev server for rapid web development

256 lines (243 loc) 10 kB
"use strict"; 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;