UNPKG

page-with

Version:

A library for usage example-driven in-browser testing of your own libraries.

217 lines (216 loc) 8.65 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PreviewServer = void 0; const fs = require("fs"); const path = require("path"); const until_1 = require("@open-draft/until"); const express = require("express"); const uuid_1 = require("uuid"); const memfs_1 = require("memfs"); const webpack_1 = require("webpack"); const webpack_merge_1 = require("webpack-merge"); const mustache_1 = require("mustache"); const staticFromMemory_1 = require("../middleware/staticFromMemory"); const asyncCompile_1 = require("../utils/asyncCompile"); const createLogger_1 = require("../internal/createLogger"); const webpack_config_1 = require("../webpack.config"); const DEFAULT_SERVER_OPTIONS = { compileInMemory: true, }; class PreviewServer { setOption(name, value) { this.options[name] = value; if (name === 'webpackConfig') { const prevBaseConfig = this.baseWebpackConfig; const nextWebpackConfig = value; this.baseWebpackConfig = (0, webpack_merge_1.default)(prevBaseConfig, nextWebpackConfig || {}); } } constructor(options = {}) { this.log = (0, createLogger_1.createLogger)('server'); this.options = { ...DEFAULT_SERVER_OPTIONS, ...options }; this.baseWebpackConfig = (0, webpack_merge_1.default)(webpack_config_1.webpackConfig, this.options.webpackConfig || {}); this.memoryFs = (0, memfs_1.createFsFromVolume)(new memfs_1.Volume()); this.cache = new Map(); this.app = express(); this.connection = null; this.connectionInfo = null; this.pages = new Map(); this.initSettings(); this.applyMiddleware(); this.applyRoutes(); } initSettings() { this.app.set('contentBase', null); } async listen(port = 0, host = 'localhost') { return new Promise((resolve, reject) => { this.log('establishing server connection...'); const connection = this.app.listen(port, host, () => { const { address, port, family } = connection.address(); // IPv6 host requires surrounding square brackets when serialized to URL // note: IPv6 host can take many forms, e.g. `::` and `::1` are both ok const serializedAddress = family === 'IPv6' ? `[${address}]` : address; const url = `http://${serializedAddress}:${port}`; this.connectionInfo = { port, host: address, url, family, }; this.connection = connection; this.log('preview server established at %s', url); resolve(this.connectionInfo); }); connection.on('error', reject); }); } async compile(entryPath) { this.log('compiling entry...', entryPath); this.log('compiling to memory?', this.options.compileInMemory); if (!entryPath) { this.log('no entry given, skipping the compilation'); return new Set(); } const absoluteEntryPath = path.isAbsolute(entryPath) ? entryPath : path.resolve(process.cwd(), entryPath); this.log('resolved absolute entry path', absoluteEntryPath); const entryStats = fs.statSync(absoluteEntryPath); const cachedEntry = this.cache.get(absoluteEntryPath); this.log('looking up a cached compilation...'); if ((cachedEntry === null || cachedEntry === void 0 ? void 0 : cachedEntry.lastModified) === entryStats.mtimeMs) { this.log('found a cached compilation!', cachedEntry.lastModified); return cachedEntry.chunks; } this.log('no cache found, compiling...'); const webpackConfig = Object.assign({}, this.baseWebpackConfig, { entry: { main: absoluteEntryPath, }, }); const compiler = (0, webpack_1.webpack)(webpackConfig); this.log('resolved webpack configuration', webpackConfig); if (!this.options.compileInMemory) { this.log('compiling to dist:', webpackConfig.output); } if (this.options.compileInMemory) { compiler.outputFileSystem = this.memoryFs; } const compilationResult = await (0, until_1.until)(() => (0, asyncCompile_1.asyncCompile)(compiler)); if (compilationResult.error) { this.log('failed to compile', absoluteEntryPath); throw compilationResult.error; } const stats = compilationResult.data; const { chunks } = stats.compilation; this.log('caching the compilation...', entryStats.mtimeMs, chunks.size); this.cache.set(absoluteEntryPath, { lastModified: entryStats.mtimeMs, chunks, }); return chunks; } createPreviewUrl(pageId) { var _a; const url = new URL(`/preview/${pageId}`, (_a = this.connectionInfo) === null || _a === void 0 ? void 0 : _a.url); this.log('created a preview URL for %s (%s)', pageId, url.toString()); return url.toString(); } createContext(entryPath, options) { const pageId = (0, uuid_1.v4)(); this.pages.set(pageId, { entryPath, options, }); const previewUrl = this.createPreviewUrl(pageId); return { previewUrl, }; } use(middleware) { const prevRoutesCount = this.app._router.stack.length; middleware(this.app); const nextRoutesCount = this.app._router.stack.length; return () => { const runtimeRoutesCount = nextRoutesCount - prevRoutesCount; this.app._router.stack.splice(-runtimeRoutesCount); }; } async close() { this.log('closing the server...'); if (!this.connection) { throw new Error('Failed to close a server: server is not running.'); } return new Promise((resolve, reject) => { var _a; (_a = this.connection) === null || _a === void 0 ? void 0 : _a.close((error) => { if (error) { return reject(error); } this.log('successfully closed the server!'); resolve(); }); }); } applyMiddleware() { this.applyContentBaseMiddleware(); this.applyMemoryFsMiddleware(); } applyContentBaseMiddleware() { this.app.use((req, res, next) => { const contentBase = this.app.get('contentBase'); if (contentBase) { return express.static(contentBase)(req, res, next); } return next(); }); } applyMemoryFsMiddleware() { this.app.use('/assets', (0, staticFromMemory_1.staticFromMemory)(this.memoryFs)); } applyRoutes() { var _a, _b; (_b = (_a = this.options).router) === null || _b === void 0 ? void 0 : _b.call(_a, this.app); this.app.get('/preview/:pageId', async (req, res) => { this.log('[get] %s', req.url); const { pageId } = req.params; const page = this.pages.get(pageId); if (!page) { return res.status(404).end(); } const chunks = await this.compile(page.entryPath); const html = this.renderHtml(chunks, pageId); return res.send(html); }); } renderHtml(chunks, pageId) { this.log('rendering html...'); const page = this.pages.get(pageId); if (!page) { return `<p>Page with ID "${pageId}" not found.</p>`; } const assets = []; for (const chunk of chunks || new Set()) { for (const filename of chunk.files) { assets.push(filename); } } const template = fs.readFileSync(path.resolve(__dirname, 'template.mustache'), 'utf8'); let markup = ''; const customMarkup = page === null || page === void 0 ? void 0 : page.options.markup; if (customMarkup) { markup = fs.existsSync(customMarkup) ? fs.readFileSync(customMarkup, 'utf8') : customMarkup; } const html = (0, mustache_1.render)(template, { title: page === null || page === void 0 ? void 0 : page.options.title, markup, assets, }); this.log('rendered html', '\n', html); return html; } } exports.PreviewServer = PreviewServer;