page-with
Version:
A library for usage example-driven in-browser testing of your own libraries.
217 lines (216 loc) • 8.65 kB
JavaScript
"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;