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`
|
API (used to verify the dependency versions/licenses in `package.json`
|
||||||
actually exist and resolve). The code is written carefully against each
|
actually exist and resolve). The code is written carefully against each
|
||||||
library's documented API (`@excalidraw/utils`'s `exportToSvg`,
|
library's documented API (`@excalidraw/utils`'s `exportToSvg`,
|
||||||
`@resvg/resvg-js`'s `Resvg` class), but the first real build
|
`@resvg/resvg-js`'s `Resvg` class), but `docker compose build`+actually
|
||||||
(`docker compose build`) is the first time this has actually executed
|
running it is the first time this has executed end to end -- and the
|
||||||
end to end. If something's off, `docker compose logs` will show it --
|
first real run did in fact crash: `@excalidraw/utils`'s bundle touches
|
||||||
most likely candidates are `exportToSvg`'s actual return type (string vs.
|
bare browser globals (`devicePixelRatio`, `location`, `matchMedia`, ...)
|
||||||
DOM element -- handled defensively, see server.js) or a font-rendering
|
the same way inline `<script>` code in a real page would, not as
|
||||||
quirk, not a wrong API shape.
|
`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)
|
## Local development (if you have Node 20.19+/22.13+ installed)
|
||||||
|
|
||||||
|
|||||||
@@ -25,15 +25,47 @@
|
|||||||
|
|
||||||
const { JSDOM } = require('jsdom');
|
const { JSDOM } = require('jsdom');
|
||||||
|
|
||||||
// @excalidraw/utils touches `window`/`document` globals even though
|
// @excalidraw/utils is a browser bundle: it references things like
|
||||||
// exportToSvg's own return value doesn't depend on a live page -- these
|
// `devicePixelRatio` and `location` as bare identifiers, the same way
|
||||||
// have to exist before the package is required, not just before it's
|
// inline <script> code in a real page would resolve them off the global
|
||||||
// called.
|
// scope -- not as `window.devicePixelRatio`. Copying jsdom's entire
|
||||||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
// `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.window = dom.window;
|
||||||
global.document = dom.window.document;
|
global.document = dom.window.document;
|
||||||
global.navigator = dom.window.navigator;
|
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 express = require('express');
|
||||||
const { exportToSvg } = require('@excalidraw/utils');
|
const { exportToSvg } = require('@excalidraw/utils');
|
||||||
const { Resvg } = require('@resvg/resvg-js');
|
const { Resvg } = require('@resvg/resvg-js');
|
||||||
|
|||||||
Reference in New Issue
Block a user