mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
Merge pull request #4139 from heygen-com/sdk-cutover-decline-reason
fix(studio): a composition with a <canvas> no longer kills the SDK session
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Browser stub for the Node-only `canvas` package.
|
||||
*
|
||||
* linkedom's `HTMLCanvasElement` constructor calls `createCanvas(300, 150)`
|
||||
* (300x150 being the HTML canvas default size). linkedom guards that import in
|
||||
* a try/catch: the try requires the native `canvas` package, and the catch
|
||||
* falls back to linkedom's own bundled canvas shim.
|
||||
*
|
||||
* That guard only works with a *runtime* require. Vite resolves the specifier
|
||||
* statically, so the bundled form is a plain assignment that cannot throw, the
|
||||
* catch never runs, and `createCanvas` lands `undefined`. Parsing any `<canvas>`
|
||||
* tag then throws inside `openComposition`, Studio loses its SDK session for the
|
||||
* whole composition, and every edit silently falls back to the server path — the
|
||||
* resolver shadow does not run either, so it was invisible until
|
||||
* `studio:sdk_session_unavailable` shipped in v0.8.47.
|
||||
*
|
||||
* Production telemetry over the 72h after the flip: 1065 of 1544 session-open
|
||||
* failures were this crash, across a large share of 197 users. Canvas is common
|
||||
* in compositions (Three.js, shaders, particle effects, charts).
|
||||
*
|
||||
* Aliasing `canvas` to this file makes the browser build resolve to a
|
||||
* measurement-only canvas, which is all linkedom's static parse needs — nothing
|
||||
* in the SDK rasterizes through it. It mirrors linkedom's own
|
||||
* `commonjs/canvas-shim.cjs`, reimplemented here because linkedom's `exports`
|
||||
* map does not expose that file.
|
||||
*/
|
||||
|
||||
class BrowserCanvasStub {
|
||||
constructor(width, height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
/** linkedom only ever needs the element to exist; nothing draws through it. */
|
||||
getContext() {
|
||||
return null;
|
||||
}
|
||||
toDataURL() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function createCanvas(width, height) {
|
||||
return new BrowserCanvasStub(width, height);
|
||||
}
|
||||
|
||||
// linkedom consumes this as a DEFAULT import — `import Canvas from
|
||||
// '../../commonjs/canvas.cjs'` then `const {createCanvas} = Canvas` — so the
|
||||
// default export is the load-bearing one; the named export above is interop
|
||||
// belt-and-braces.
|
||||
export default { createCanvas };
|
||||
@@ -0,0 +1,77 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
|
||||
/**
|
||||
* linkedom's HTMLCanvasElement constructor calls `createCanvas(300, 150)` from
|
||||
* the Node-only `canvas` package. linkedom guards that import in a try/catch —
|
||||
* try the native package, fall back to its own bundled shim — but a bundler
|
||||
* resolves the require statically, so the catch never fires in the browser
|
||||
* build and `createCanvas` lands undefined. Parsing any `<canvas>` tag then
|
||||
* throws, `openComposition` rejects, and Studio silently loses its SDK session
|
||||
* for the whole composition (no cutover, and the resolver shadow does not run
|
||||
* either).
|
||||
*
|
||||
* Production telemetry, 72h after the v0.8.47 flip: 1065 of 1544
|
||||
* `studio:sdk_session_unavailable` events were this crash, spread across a
|
||||
* large share of 197 users. Canvas is common in compositions — Three.js,
|
||||
* shaders, particle effects, charts.
|
||||
*
|
||||
* The fix is a `resolve.alias` in the studio's vite config pointing `canvas` at
|
||||
* linkedom's own shim. This test pins the behaviour the alias has to preserve.
|
||||
*/
|
||||
const CANVAS_COMPOSITION = `<!doctype html>
|
||||
<html>
|
||||
<body data-composition-id="main" data-width="1920" data-height="1080">
|
||||
<div class="clip" data-start="0" data-duration="3">
|
||||
<canvas id="scene" width="800" height="600"></canvas>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
describe("openComposition with a <canvas> element", () => {
|
||||
it("opens a session instead of throwing on createCanvas", async () => {
|
||||
const session = await openComposition(CANVAS_COMPOSITION, { history: false });
|
||||
expect(session).toBeTruthy();
|
||||
session.dispose();
|
||||
});
|
||||
|
||||
it("models the canvas element so edits can target it", async () => {
|
||||
const session = await openComposition(CANVAS_COMPOSITION, { history: false });
|
||||
// A composition whose elements cannot be modelled is a session the cutover
|
||||
// path declines wholesale (sessionEmpty), which is the same silent bypass by
|
||||
// a different route.
|
||||
expect(session.getElements().length).toBeGreaterThan(0);
|
||||
session.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe("canvas browser-stub alias", () => {
|
||||
// The two tests above pass with OR without the fix: vitest resolves
|
||||
// `require('canvas')` at runtime, so linkedom's own try/catch fallback works
|
||||
// here. Only the bundled build breaks. Nothing else in this suite can observe
|
||||
// the bundler, so the alias itself is what has to be pinned — without this,
|
||||
// deleting the alias reintroduces a crash that is invisible until it reaches
|
||||
// production telemetry.
|
||||
it("is configured, so the browser build never resolves the Node canvas package", () => {
|
||||
const config = readFileSync(resolve(import.meta.dirname, "../../vite.config.ts"), "utf8");
|
||||
expect(config).toMatch(
|
||||
/\bcanvas:\s*resolve\(__dirname,\s*"src\/shims\/canvasBrowserStub\.js"\)/,
|
||||
);
|
||||
});
|
||||
|
||||
it("exports the createCanvas shape linkedom destructures", async () => {
|
||||
const stub = await import("../shims/canvasBrowserStub.js");
|
||||
// linkedom takes the DEFAULT import and destructures createCanvas off it
|
||||
// (`import Canvas from '...'; const {createCanvas} = Canvas`), so the
|
||||
// default export is the one that actually has to carry the function.
|
||||
expect(typeof stub.default.createCanvas).toBe("function");
|
||||
expect(typeof stub.createCanvas).toBe("function");
|
||||
const canvas = stub.default.createCanvas(300, 150);
|
||||
expect(canvas.width).toBe(300);
|
||||
expect(canvas.height).toBe(150);
|
||||
expect(canvas.getContext()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -244,6 +244,13 @@ export default defineConfig({
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// linkedom's HTMLCanvasElement constructor calls createCanvas(300, 150)
|
||||
// from the Node-only `canvas` package, behind a
|
||||
// `try { require('canvas') } catch { shim }` guard. A bundler resolves
|
||||
// that require statically, so the catch never fires and createCanvas is
|
||||
// undefined — every composition containing a <canvas> then throws inside
|
||||
// openComposition and silently loses its SDK session. See the stub.
|
||||
canvas: resolve(__dirname, "src/shims/canvasBrowserStub.js"),
|
||||
"@hyperframes/player": resolve(__dirname, "../player/src/hyperframes-player.ts"),
|
||||
"@hyperframes/studio-server/source-mutation": resolve(
|
||||
__dirname,
|
||||
|
||||
Reference in New Issue
Block a user