107 lines
3.6 KiB
JavaScript
107 lines
3.6 KiB
JavaScript
'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(`
|
|
<!doctype html>
|
|
<html>
|
|
<head></head>
|
|
<body>
|
|
<div id="app" class="hydrogen"></div>
|
|
</body>
|
|
</html>
|
|
`);
|
|
|
|
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',
|
|
`
|
|
<script type="text/javascript" nonce="${renderOptions.pageOptions.cspNonce}">
|
|
window.matrixPublicArchiveContext = ${safeJson(serializedContext)}
|
|
</script>
|
|
`
|
|
);
|
|
|
|
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;
|