Fix whiteboard render sidecar crash on startup
Build and push server image / build-and-push (push) Successful in 1m57s

@excalidraw/utils is a browser bundle -- it references devicePixelRatio,
location, matchMedia etc. as bare globals the way inline <script> code
would, not as window.foo. Only copying window/document/navigator onto
Node's global left everything else undefined, so the very first bare
reference threw a ReferenceError as soon as the module loaded. Copy
jsdom's entire window onto global instead, enable pretendToBeVisual so
jsdom actually populates devicePixelRatio/requestAnimationFrame, and
stub matchMedia since jsdom doesn't implement it at all.
This commit is contained in:
2026-07-23 17:50:27 -04:00
parent c171047adf
commit dadd9ec164
2 changed files with 54 additions and 11 deletions
+17 -6
View File
@@ -13,12 +13,23 @@ that same container.
API (used to verify the dependency versions/licenses in `package.json`
actually exist and resolve). The code is written carefully against each
library's documented API (`@excalidraw/utils`'s `exportToSvg`,
`@resvg/resvg-js`'s `Resvg` class), but the first real build
(`docker compose build`) is the first time this has actually executed
end to end. If something's off, `docker compose logs` will show it --
most likely candidates are `exportToSvg`'s actual return type (string vs.
DOM element -- handled defensively, see server.js) or a font-rendering
quirk, not a wrong API shape.
`@resvg/resvg-js`'s `Resvg` class), but `docker compose build`+actually
running it is the first time this has executed end to end -- and the
first real run did in fact crash: `@excalidraw/utils`'s bundle touches
bare browser globals (`devicePixelRatio`, `location`, `matchMedia`, ...)
the same way inline `<script>` code in a real page would, not as
`window.foo` -- jsdom only puts those *on* `dom.window`, so only
copying `window`/`document`/`navigator` onto Node's `global` (the
original version of this file) left everything else undefined. Fixed
by copying jsdom's entire `window` onto `global` plus a `matchMedia`
stub (jsdom doesn't implement it at all). If another crash like this
shows up, it's almost certainly the same shape -- another bare global
the bundle expects that this file hasn't stubbed yet. One other gap
worth knowing about going in: jsdom's `<canvas>` has no real 2D
rendering context (no `node-canvas` installed), so if Excalidraw's text
measurement path depends on `canvas.getContext('2d').measureText(...)`
rather than pure SVG/font-metrics math, that could be a next thing to
watch for -- not something confirmed broken, just not yet exercised.
## Local development (if you have Node 20.19+/22.13+ installed)
+37 -5
View File
@@ -25,15 +25,47 @@
const { JSDOM } = require('jsdom');
// @excalidraw/utils touches `window`/`document` globals even though
// exportToSvg's own return value doesn't depend on a live page -- these
// have to exist before the package is required, not just before it's
// called.
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
// @excalidraw/utils is a browser bundle: it references things like
// `devicePixelRatio` and `location` as bare identifiers, the same way
// inline <script> code in a real page would resolve them off the global
// scope -- not as `window.devicePixelRatio`. Copying jsdom's entire
// `window` onto Node's `global` (not just window/document/navigator) is
// what makes those bare references resolve at all; without it, the first
// one touched throws a ReferenceError. `pretendToBeVisual` is what makes
// jsdom actually populate devicePixelRatio/requestAnimationFrame in the
// first place -- both are left undefined otherwise.
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', { pretendToBeVisual: true });
for (const key of Object.getOwnPropertyNames(dom.window)) {
if (key in global) continue;
try {
global[key] = dom.window[key];
} catch {
// a handful of window properties throw on read outside a real
// browser (e.g. some storage/permissions getters) -- skip those
// rather than let one bad property crash startup entirely
}
}
global.window = dom.window;
global.document = dom.window.document;
global.navigator = dom.window.navigator;
// jsdom doesn't implement matchMedia -- Excalidraw's bundle calls it
// unconditionally (theme/print-media detection), so without a stub this
// is the next ReferenceError-shaped crash after the one above.
if (typeof global.window.matchMedia !== 'function') {
const stubMatchMedia = () => ({
matches: false,
media: '',
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
});
global.window.matchMedia = stubMatchMedia;
global.matchMedia = stubMatchMedia;
}
const express = require('express');
const { exportToSvg } = require('@excalidraw/utils');
const { Resvg } = require('@resvg/resvg-js');