UNPKG

html-render-webpack-plugin

Version:

webpack plugin for rendering static HTML in a multi-config webpack build

245 lines 11.5 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HtmlRenderPlugin = void 0; const schema_utils_1 = require("schema-utils"); const HtmlRenderWebpackPlugin_json_1 = __importDefault(require("./schemas/HtmlRenderWebpackPlugin.json")); const RenderError_1 = __importDefault(require("./RenderError")); const renderRoutes_1 = __importDefault(require("./renderRoutes")); const logging_1 = require("./logging"); const MultiStats_1 = __importDefault(require("webpack/lib/MultiStats")); const createDevRouter_1 = __importDefault(require("./createDevRouter")); const createRenderer_1 = __importDefault(require("./createRenderer")); const timeSince = (startTime) => `${(Date.now() - startTime) / 1000}s`; const defaultMapStats = ({ webpackStats }) => webpackStats ? { webpackStats: webpackStats.toJson() } : {}; const defaultTransform = (route) => route.route; class HtmlRenderPlugin { constructor(options = {}) { schema_utils_1.validate(HtmlRenderWebpackPlugin_json_1.default, options || {}, { name: "HTML Render Webpack Plugin", }); const pluginName = "HtmlRenderPlugin"; const { extraGlobals = {}, skipAssets = false, mapStatsToParams = defaultMapStats, renderEntry = "main", getRouteFromRequest, transformFilePath = defaultTransform, transformExpressPath = defaultTransform, renderConcurrency = "serial", } = options; const routes = (options.routes || [""]).map((route) => typeof route === "string" ? { route } : route); const renderDirectory = options.renderDirectory || "dist"; const clientCompilations = []; let rendererCompilation; let renderer; let lastClientStats = null; const isBuildReady = () => rendererCompilation && rendererCompilation.isReady && clientCompilations.every((compilationStatus) => compilationStatus.isReady); const isRendererReady = () => isBuildReady() && Boolean(renderer); const renderQueue = []; const flushRenderQueue = () => __awaiter(this, void 0, void 0, function* () { if (isRendererReady() && renderQueue.length > 0) { yield renderQueue.shift()(); flushRenderQueue(); } }); const render = (route) => __awaiter(this, void 0, void 0, function* () { const startRenderTime = Date.now(); logging_1.log(`Starting render`, route); const webpackStats = getClientStats(); const renderParams = Object.assign(Object.assign({}, route), mapStatsToParams(Object.assign(Object.assign({}, route), { webpackStats }))); try { const result = yield renderer(renderParams); logging_1.log(`Successfully rendered ${route.route} (${timeSince(startRenderTime)})`); return result; } catch (error) { logging_1.log(`Error rendering ${route.route}`); if (error) { error.route = route.route; error.webpackStats = webpackStats; } throw error; } }); const onRenderAll = (currentCompilation) => __awaiter(this, void 0, void 0, function* () { logging_1.log(`Starting routes render`); if (!rendererCompilation.compilation) { const errorMessage = `Unable to find render compilation. Something may have gone wrong during render build.`; logging_1.logError(errorMessage); throw new Error(errorMessage); } try { yield renderRoutes_1.default({ render, renderConcurrency, renderCompilation: rendererCompilation.compilation, renderDirectory, renderEntry, routes, transformFilePath, }); } catch (error) { logging_1.logError("An error occurred rendering HTML", error); // @ts-expect-error Allow passing errors to compilation currentCompilation.errors.push(new RenderError_1.default(error)); } logging_1.log(`Ending routes render`); }); const getRenderEntry = (compilation) => { const renderStats = compilation.getStats().toJson(); const asset = renderStats.assetsByChunkName[renderEntry]; if (!asset) { throw new Error(`Unable to find renderEntry "${renderEntry}" in assets. Possible entries are: ${Object.keys(renderStats.assetsByChunkName).join(", ")}.`); } let renderFile = asset; if (Array.isArray(renderFile)) { renderFile = renderFile[0]; } if (renderFile && typeof renderFile === "object") { renderFile = renderFile.name; } return renderFile; }; const renderCallbacks = []; const getClientStats = () => { if (lastClientStats) { return lastClientStats; } const clientStats = clientCompilations.length === 1 ? clientCompilations[0].compilation.getStats() : new MultiStats_1.default(clientCompilations .map((compilationStatus) => compilationStatus.compilation) .filter(Boolean) .map((compilation) => compilation.getStats())); lastClientStats = clientStats; return clientStats; }; const flushQueuedRenders = () => { if (isRendererReady() && renderCallbacks.length > 0) { renderCallbacks.shift()(renderer, getClientStats()); flushQueuedRenders(); } }; const onRendererReady = (cb) => { if (isRendererReady()) { cb(render); } else { renderCallbacks.push(cb); } }; const createRendererIfReady = (currentCompilation) => __awaiter(this, void 0, void 0, function* () { if (!isBuildReady()) { return; } const renderCompilation = rendererCompilation.compilation; const renderEntry = getRenderEntry(renderCompilation); logging_1.log("Render route:", { renderEntry }); const rootDir = renderCompilation.compiler.outputPath; renderer = createRenderer_1.default({ fileSystem: renderCompilation.compiler.outputFileSystem, fileName: renderEntry, rootDir, extraGlobals, }); logging_1.log("Created renderer"); if (typeof renderer !== "function") { logging_1.log(`Unable to find render function. File "${renderEntry}". Received ${typeof renderer}.`); throw new Error(`Unable to find render function. File "${renderEntry}". Received ${typeof renderer}.`); } flushQueuedRenders(); flushRenderQueue(); logging_1.log("Queued and flushed"); if (!skipAssets) { yield onRenderAll(currentCompilation); logging_1.log("onRenderAll complete"); } }); const apply = (compiler, isRenderer) => { const compilerName = compiler.name || compiler.options.name; const compilationStatus = { compilation: null, isReady: false, }; if (isRenderer) { logging_1.log(`Received render compiler: ${compilerName}`); } else { logging_1.log(`Received compiler: ${compilerName}`); } if (isRenderer) { if (rendererCompilation) { throw new Error("Error. Unable to apply a second renderer"); } rendererCompilation = compilationStatus; } else { clientCompilations.push(compilationStatus); } compiler.hooks.watchRun.tap(pluginName, () => { logging_1.log(`Build started for for ${compilerName}.`); compilationStatus.isReady = false; }); compiler.hooks.afterEmit.tapPromise(pluginName, (compilation) => __awaiter(this, void 0, void 0, function* () { logging_1.log(`Assets emitted for ${compilerName}.`); compilationStatus.compilation = compilation; lastClientStats = null; compilationStatus.isReady = true; try { yield createRendererIfReady(compilation); } catch (error) { compilation.errors.push(error); } })); }; this.statsCollectorPlugin = (compiler) => apply(compiler, false); this.rendererPlugin = (compiler) => { // Support legacy behaviour of calling '.render()' until next breaking change if (!compiler) { console.warn("Warning. Calling render no longer required. Change htmlRenderPlugin.render() to htmlRenderPlugin.render"); return (compiler) => apply(compiler, true); } return apply(compiler, true); }; this.apply = (compiler) => { console.warn("Warning. Attempted to apply directly to webpack. Use htmlRenderPlugin.statsCollectorPlugin instead."); this.statsCollectorPlugin(compiler); }; this.render = () => this.rendererPlugin; this.createDevRouter = () => createDevRouter_1.default({ transformExpressPath, getRouteFromRequest, onRendererReady, getClientStats, routes, }); this.renderWhenReady = (route) => new Promise((resolve, reject) => { const onRender = () => { logging_1.log("Rendering renderWhenReady onRender"); try { resolve(render(route)); } catch (error) { reject(error); } }; if (isRendererReady()) { onRender(); } else { renderQueue.push(onRender); } }); } } exports.default = HtmlRenderPlugin; exports.HtmlRenderPlugin = HtmlRenderPlugin; //# sourceMappingURL=index.js.map