html-render-webpack-plugin
Version:
webpack plugin for rendering static HTML in a multi-config webpack build
245 lines • 11.5 kB
JavaScript
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
;