'use strict'; // Server-side render Hydrogen to a string using a browser-like context thanks // to `linkedom`. We use a VM so we can put all of the browser-like globals in // place. // // Note: This is marked as unsafe because the render script is run in a VM which // doesn't exit after we get the result (Hydrogen keeps running). There isn't a // way to stop, terminate, or kill a vm script or vm context so in order to be // safe, we need to run this inside of a child_process which we can kill after. // This is why we have the `1-render-hydrogen-to-string.js` layer to handle // this. const assert = require('assert'); const vm = require('vm'); const path = require('path'); const { readFile } = require('fs').promises; const crypto = require('crypto'); const { parseHTML } = require('linkedom'); const safeJson = require('../lib/safe-json'); // Setup the DOM context with any necessary shims/polyfills and ensure the VM // context global has everything that a normal document does so Hydrogen can // render. function createDomAndSetupVmContext() { const dom = parseHTML(`
`); if (!dom.requestAnimationFrame) { dom.requestAnimationFrame = function (cb) { setTimeout(cb, 0); }; } const vmContext = vm.createContext(dom); // Make the dom properties available in sub-`require(...)` calls vmContext.global.window = dom.window; vmContext.global.document = dom.document; vmContext.global.Node = dom.Node; vmContext.global.navigator = dom.navigator; vmContext.global.DOMParser = dom.DOMParser; // Make sure `webcrypto` exists since it was only introduced in Node.js v17 assert(crypto.webcrypto); vmContext.global.crypto = crypto.webcrypto; // So require(...) works in the vm vmContext.global.require = require; // So we can see logs from the underlying vm vmContext.global.console = console; return { dom, vmContext, }; } async function _renderHydrogenToStringUnsafe(renderOptions) { assert(renderOptions); assert(renderOptions.vmRenderScriptFilePath); assert(renderOptions.vmRenderContext); assert(renderOptions.pageOptions); assert(renderOptions.pageOptions.cspNonce); const { dom, vmContext } = createDomAndSetupVmContext(); // Define this for the SSR context dom.window.matrixPublicArchiveContext = { ...renderOptions.vmRenderContext, }; // Serialize it for when we run this again client-side const serializedContext = JSON.stringify(dom.window.matrixPublicArchiveContext); dom.document.body.insertAdjacentHTML( 'beforeend', ` ` ); const vmRenderScriptFilePath = renderOptions.vmRenderScriptFilePath; const hydrogenRenderScriptCode = await readFile(vmRenderScriptFilePath, 'utf8'); const hydrogenRenderScript = new vm.Script(hydrogenRenderScriptCode, { filename: path.basename(vmRenderScriptFilePath), }); // Note: The VM does not exit after the result is returned here and is why // this should be run in a `child_process` that we can exit. const vmResult = hydrogenRenderScript.runInContext(vmContext); // Wait for everything to render // (waiting on the promise returned from the VM render script) await vmResult; const documentString = dom.document.body.toString(); assert(documentString, 'Document body should not be empty after we rendered Hydrogen'); return documentString; } module.exports = _renderHydrogenToStringUnsafe;