2022-07-05 16:30:52 -06:00
|
|
|
'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');
|
|
|
|
|
2022-08-29 18:42:18 -06:00
|
|
|
// 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() {
|
2022-07-05 16:30:52 -06:00
|
|
|
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);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-08-29 18:42:18 -06:00
|
|
|
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);
|
2022-11-18 11:27:50 -07:00
|
|
|
// Only assign vmContext.global.crypto if it's undefined
|
|
|
|
// (Node.js v19 has crypto set on the global already)
|
|
|
|
if (!vmContext.global.crypto) {
|
|
|
|
vmContext.global.crypto = crypto.webcrypto;
|
|
|
|
}
|
2022-08-29 18:42:18 -06:00
|
|
|
|
|
|
|
// 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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-09-02 19:49:06 -06:00
|
|
|
async function _renderHydrogenToStringUnsafe(renderOptions) {
|
|
|
|
assert(renderOptions);
|
|
|
|
assert(renderOptions.vmRenderScriptFilePath);
|
|
|
|
assert(renderOptions.vmRenderContext);
|
2022-10-19 11:07:39 -06:00
|
|
|
assert(renderOptions.pageOptions);
|
2022-11-09 17:57:33 -07:00
|
|
|
assert(renderOptions.pageOptions.locationHref);
|
2022-10-19 11:07:39 -06:00
|
|
|
assert(renderOptions.pageOptions.cspNonce);
|
2022-08-29 18:42:18 -06:00
|
|
|
|
|
|
|
const { dom, vmContext } = createDomAndSetupVmContext();
|
|
|
|
|
2022-11-09 17:57:33 -07:00
|
|
|
// A small `window.location` stub
|
|
|
|
if (!dom.window.location) {
|
|
|
|
const locationUrl = new URL(renderOptions.pageOptions.locationHref);
|
|
|
|
dom.window.location = {};
|
|
|
|
[
|
|
|
|
'hash',
|
|
|
|
'host',
|
|
|
|
'hostname',
|
|
|
|
'href',
|
|
|
|
'origin',
|
|
|
|
'password',
|
|
|
|
'pathname',
|
|
|
|
'port',
|
|
|
|
'protocol',
|
|
|
|
'search',
|
|
|
|
'username',
|
|
|
|
].forEach((key) => {
|
|
|
|
dom.window.location[key] = locationUrl[key];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-07-05 16:30:52 -06:00
|
|
|
// Define this for the SSR context
|
|
|
|
dom.window.matrixPublicArchiveContext = {
|
2022-09-02 19:49:06 -06:00
|
|
|
...renderOptions.vmRenderContext,
|
2022-07-05 16:30:52 -06:00
|
|
|
};
|
|
|
|
|
2022-09-02 19:49:06 -06:00
|
|
|
const vmRenderScriptFilePath = renderOptions.vmRenderScriptFilePath;
|
|
|
|
const hydrogenRenderScriptCode = await readFile(vmRenderScriptFilePath, 'utf8');
|
2022-07-05 16:30:52 -06:00
|
|
|
const hydrogenRenderScript = new vm.Script(hydrogenRenderScriptCode, {
|
2022-09-02 19:49:06 -06:00
|
|
|
filename: path.basename(vmRenderScriptFilePath),
|
2022-07-05 16:30:52 -06:00
|
|
|
});
|
2022-08-29 18:13:56 -06:00
|
|
|
// 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.
|
2022-07-05 16:30:52 -06:00
|
|
|
const vmResult = hydrogenRenderScript.runInContext(vmContext);
|
|
|
|
// Wait for everything to render
|
2022-09-02 19:49:06 -06:00
|
|
|
// (waiting on the promise returned from the VM render script)
|
2022-07-05 16:30:52 -06:00
|
|
|
await vmResult;
|
|
|
|
|
|
|
|
const documentString = dom.document.body.toString();
|
2022-07-06 18:24:29 -06:00
|
|
|
assert(documentString, 'Document body should not be empty after we rendered Hydrogen');
|
2022-07-05 16:30:52 -06:00
|
|
|
return documentString;
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = _renderHydrogenToStringUnsafe;
|