diff --git a/server/lib/rethrown-error.js b/server/lib/rethrown-error.js new file mode 100644 index 0000000..85ca8a8 --- /dev/null +++ b/server/lib/rethrown-error.js @@ -0,0 +1,61 @@ +'use strict'; + +// via https://stackoverflow.com/a/42755876/796832 + +// Standard error extender from @deployable/errors +class ExtendedError extends Error { + constructor(message) { + super(message); + this.name = this.constructor.name; + this.message = message; + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } else { + this.stack = new Error(message).stack; + } + } +} + +// A way to create a new error with a custom message but keep the stack trace of +// the original error. Useful to give more context and why the action was tried +// in the first place. +// +// For example, if you get a generic EACCES disk error of a certain file, you +// want to know why and what context the disk was trying to be read. A +// stack-trace is not human digestable and only gives the where in the code. +// What I actually need to know is that I was trying to read the `ratelimit` key +// from the config when this error occured. +// +// `new RethrownError('Failed to get the ratelimit key from the config', originalError)` (failed to read the disk) +class RethrownError extends ExtendedError { + constructor(message, error) { + super(message); + if (!error) throw new Error('RethrownError requires a message and error'); + this.original = error; + this.newStack = this.stack; + + // The number of lines that make up the message itself. We count this by the + // number of `\n` and `+ 1` for the first line because it doesn't start with + // new line. + const messageLines = (this.message.match(/\n/g) || []).length + 1; + console.log('messageLines', messageLines); + + const indentedOriginalError = error.stack + .split('\n') + .map((line) => ` ${line}`) + .join('\n'); + + this.stack = + this.stack + .split('\n') + // We use `+ 1` here so that we include the first line of the stack to + // people know where the error was thrown from. + .slice(0, messageLines + 1) + .join('\n') + + '\n' + + ' --- Original Error ---\n' + + indentedOriginalError; + } +} + +module.exports = RethrownError; diff --git a/server/render-hydrogen-to-string.js b/server/render-hydrogen-to-string.js index e5fbaed..f8dd127 100644 --- a/server/render-hydrogen-to-string.js +++ b/server/render-hydrogen-to-string.js @@ -7,82 +7,92 @@ const { readFile } = require('fs').promises; const crypto = require('crypto'); const { parseHTML } = require('linkedom'); +const RethrownError = require('./lib/rethrown-error'); const config = require('./lib/config'); -async function renderToString({ fromTimestamp, roomData, events, stateEventMap }) { - assert(fromTimestamp); - assert(roomData); - assert(events); - assert(stateEventMap); +async function renderHydrogenToString({ fromTimestamp, roomData, events, stateEventMap }) { + try { + assert(fromTimestamp); + assert(roomData); + assert(events); + assert(stateEventMap); - const dom = parseHTML(` - - - - -
- - - `); + const dom = parseHTML(` + + + + +
+ + + `); - if (!dom.requestAnimationFrame) { - dom.requestAnimationFrame = function (cb) { - setTimeout(cb, 0); + if (!dom.requestAnimationFrame) { + dom.requestAnimationFrame = function (cb) { + setTimeout(cb, 0); + }; + } + + // Define this for the SSR context + dom.window.matrixPublicArchiveContext = { + fromTimestamp, + roomData, + events, + stateEventMap, + config: { + basePort: config.get('basePort'), + basePath: config.get('basePath'), + matrixServerUrl: config.get('matrixServerUrl'), + }, }; + // Serialize it for when we run this again client-side + dom.document.body.insertAdjacentHTML( + 'beforeend', + ` + + ` + ); + + 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; + + const hydrogenRenderScriptCode = await readFile( + path.resolve(__dirname, '../shared/hydrogen-vm-render-script.js'), + 'utf8' + ); + const hydrogenRenderScript = new vm.Script(hydrogenRenderScriptCode, { + filename: 'hydrogen-vm-render-script.js', + }); + const vmResult = hydrogenRenderScript.runInContext(vmContext); + // Wait for everything to render + // (waiting on the promise returned from `hydrogen-render-script.js`) + await vmResult; + + const documentString = dom.document.body.toString(); + return documentString; + } catch (err) { + throw new RethrownError( + `Failed to render Hydrogen to string. In order to reproduce, feed in these arguments into \`renderHydrogenToString(...)\`:\n renderToString arguments: ${JSON.stringify( + arguments[0] + )}`, + err + ); } - - // Define this for the SSR context - dom.window.matrixPublicArchiveContext = { - fromTimestamp, - roomData, - events, - stateEventMap, - config: { - basePort: config.get('basePort'), - basePath: config.get('basePath'), - matrixServerUrl: config.get('matrixServerUrl'), - }, - }; - // Serialize it for when we run this again client-side - dom.document.body.insertAdjacentHTML( - 'beforeend', - ` - - ` - ); - - 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; - - const hydrogenRenderScriptCode = await readFile( - path.resolve(__dirname, '../shared/hydrogen-vm-render-script.js'), - 'utf8' - ); - const hydrogenRenderScript = new vm.Script(hydrogenRenderScriptCode, { - filename: 'hydrogen-vm-render-script.js', - }); - const vmResult = hydrogenRenderScript.runInContext(vmContext); - // Wait for everything to render - // (waiting on the promise returned from `hydrogen-render-script.js`) - await vmResult; - - const documentString = dom.document.body.toString(); - return documentString; } -module.exports = renderToString; +module.exports = renderHydrogenToString;