Fix whiteboard render sidecar crash on startup
Build and push server image / build-and-push (push) Successful in 1m57s
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user