matrix-public-archive/server/hydrogen-render/render-hydrogen-to-string-u...

107 lines
3.6 KiB
JavaScript
Raw Normal View History

'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');
Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events (#43) Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events. Like if someone is replying to an event that was from long ago out of our range. In the case of missing relations, Hydrogen does `_loadContextEntryNotInTimeline` because it can't find the event locally which throws an `uncaughtException`. Before https://github.com/matrix-org/matrix-public-archive/pull/51, the `uncaughtException` killed the Hydrogen `child_process` before it could pass back the HTML. Now this PR mainly just adds a test to make sure it works. ``` TypeError: Cannot read properties of undefined (reading 'storeNames') at TimelineReader.readById (hydrogen-web\target\lib-build\hydrogen.cjs.js:12483:33) at Timeline._getEventFromStorage (hydrogen-web\target\lib-build\hydrogen.cjs.js:12762:46) at Timeline._loadContextEntryNotInTimeline (hydrogen-web\target\lib-build\hydrogen.cjs.js:12747:35) at Timeline._loadContextEntriesWhereNeeded (hydrogen-web\target\lib-build\hydrogen.cjs.js:12741:14) at Timeline.addEntries (hydrogen-web\target\lib-build\hydrogen.cjs.js:12699:10) at mountHydrogen (4-hydrogen-vm-render-script.js:204:12) at 4-hydrogen-vm-render-script.js:353:1 at Script.runInContext (node:vm:139:12) at _renderHydrogenToStringUnsafe (matrix-public-archive\server\hydrogen-render\3-render-hydrogen-to-string-unsafe.js:102:41) at async process.<anonymous> (matrix-public-archive\server\hydrogen-render\2-render-hydrogen-to-string-fork-script.js:18:27) ```
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() {
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);
};
}
Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events (#43) Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events. Like if someone is replying to an event that was from long ago out of our range. In the case of missing relations, Hydrogen does `_loadContextEntryNotInTimeline` because it can't find the event locally which throws an `uncaughtException`. Before https://github.com/matrix-org/matrix-public-archive/pull/51, the `uncaughtException` killed the Hydrogen `child_process` before it could pass back the HTML. Now this PR mainly just adds a test to make sure it works. ``` TypeError: Cannot read properties of undefined (reading 'storeNames') at TimelineReader.readById (hydrogen-web\target\lib-build\hydrogen.cjs.js:12483:33) at Timeline._getEventFromStorage (hydrogen-web\target\lib-build\hydrogen.cjs.js:12762:46) at Timeline._loadContextEntryNotInTimeline (hydrogen-web\target\lib-build\hydrogen.cjs.js:12747:35) at Timeline._loadContextEntriesWhereNeeded (hydrogen-web\target\lib-build\hydrogen.cjs.js:12741:14) at Timeline.addEntries (hydrogen-web\target\lib-build\hydrogen.cjs.js:12699:10) at mountHydrogen (4-hydrogen-vm-render-script.js:204:12) at 4-hydrogen-vm-render-script.js:353:1 at Script.runInContext (node:vm:139:12) at _renderHydrogenToStringUnsafe (matrix-public-archive\server\hydrogen-render\3-render-hydrogen-to-string-unsafe.js:102:41) at async process.<anonymous> (matrix-public-archive\server\hydrogen-render\2-render-hydrogen-to-string-fork-script.js:18:27) ```
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);
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);
Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events (#43) Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events. Like if someone is replying to an event that was from long ago out of our range. In the case of missing relations, Hydrogen does `_loadContextEntryNotInTimeline` because it can't find the event locally which throws an `uncaughtException`. Before https://github.com/matrix-org/matrix-public-archive/pull/51, the `uncaughtException` killed the Hydrogen `child_process` before it could pass back the HTML. Now this PR mainly just adds a test to make sure it works. ``` TypeError: Cannot read properties of undefined (reading 'storeNames') at TimelineReader.readById (hydrogen-web\target\lib-build\hydrogen.cjs.js:12483:33) at Timeline._getEventFromStorage (hydrogen-web\target\lib-build\hydrogen.cjs.js:12762:46) at Timeline._loadContextEntryNotInTimeline (hydrogen-web\target\lib-build\hydrogen.cjs.js:12747:35) at Timeline._loadContextEntriesWhereNeeded (hydrogen-web\target\lib-build\hydrogen.cjs.js:12741:14) at Timeline.addEntries (hydrogen-web\target\lib-build\hydrogen.cjs.js:12699:10) at mountHydrogen (4-hydrogen-vm-render-script.js:204:12) at 4-hydrogen-vm-render-script.js:353:1 at Script.runInContext (node:vm:139:12) at _renderHydrogenToStringUnsafe (matrix-public-archive\server\hydrogen-render\3-render-hydrogen-to-string-unsafe.js:102:41) at async process.<anonymous> (matrix-public-archive\server\hydrogen-render\2-render-hydrogen-to-string-fork-script.js:18:27) ```
2022-08-29 18:42:18 -06:00
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;