fix: make @playcanvas/react importable without a DOM (SSR/SSG) - #338
Merged
Conversation
Engine 2.20.0-2.20.4 calls getBoundingClientRect() on the canvas from the GraphicsDevice constructor. getNullApplication() runs at module scope with a bare-object mock canvas, so merely importing the package crashed in Node (Docusaurus SSG, Next.js builds, etc). - Stub getBoundingClientRect (and width/height) on the mock canvas so it answers layout probes without a DOM - Add a node-environment vitest project (no jsdom, no playcanvas mocks) with regression tests: package entry imports cleanly, and the mock canvas survives GraphicsDevice.updateClientRect() - Fix a latent import extension typo in gltf/index.ts (use-entity.ts -> use-entity.tsx) that strict Node resolution rejects Fixes #335
🦋 Changeset detectedLatest commit: 254d4c7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes an SSR/SSG import-time crash in @playcanvas/react by hardening the module-scope “null application” mock canvas so it can survive PlayCanvas engine layout probes in DOM-less (Node) environments across the package’s advertised peer range.
Changes:
- Stub
getBoundingClientRect()(pluswidth/height) on the null app’s mock canvas to preventGraphicsDeviceconstructor crashes in Node/SSR. - Add a dedicated Vitest “node” project (no jsdom) and regression tests to ensure the package imports cleanly and the probe path doesn’t throw.
- Fix a strict-resolution path typo by updating the GLTF hook re-export to the correct
.tsxextension, and add a changeset for the patch release.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/lib/vitest.config.ts | Splits tests into jsdom and node projects to exercise SSR-like conditions. |
| packages/lib/src/utils/validation.ts | Adds a DOM-less-safe mock canvas surface (getBoundingClientRect, width, height) for the null application. |
| packages/lib/src/utils/validation.node.test.ts | Adds Node-environment regression tests for import safety and the engine’s client-rect probe. |
| packages/lib/src/gltf/index.ts | Fixes the useEntity re-export to reference the actual .tsx source file. |
| .changeset/ssr-mock-canvas-client-rect.md | Documents the SSR/SSG import crash fix as a patch changeset. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #335
Problem
getNullApplication()runs at module scope (validation.ts) and constructs anApplicationaround a bare-object mock canvas. Engine 2.20.0–2.20.4 callsgetBoundingClientRect()unconditionally from theGraphicsDeviceconstructor, so merely importing@playcanvas/reactin Node crashes:This breaks any SSR/SSG build (the Docusaurus build of developer.playcanvas.com, Next.js
next build, …). A'use client'boundary doesn't help — frameworks still evaluate the module in Node to prerender the HTML shell.The engine made the probe optional again in 2.21.0 (playcanvas/engine#9000), but our peer range is
"playcanvas": "^2.11.8", which still includes the broken 2.20.x window.Fix
getBoundingClientRect()(zeroed rect) andwidth/heighton the mock canvas so it answers layout probes without a DOM. This keeps the entire advertised peer range importable in Node. The mock canvas is only ever used by the null application, so this cannot affect runtime behavior.gltf/index.tsimported./hooks/use-entity.tsbut the file isuse-entity.tsx. Vite's lenient resolver hid it; strict Node ESM resolution rejects it.Why not the issue's "lazy-initialize
localApp" proposal: it doesn't help. Every component module (Light.tsx,Camera.tsx, …) callscreateComponentDefinition()at module scope, which eagerly instantiates a mock component viagetStaticNullApplication()to derive its prop schema. Laziness would only relocate the crash fromvalidation.tsto the first component module. Making schema derivation lazy is a larger refactor that this fix makes unnecessary for now.Why not bump the peer floor to
^2.21.0instead: peer ranges are metadata, not enforcement — pnpm/yarn users on 2.20.x would just get an ignorable warning and still crash, while users happily on 2.12–2.19 would be pushed into engine upgrades they don't need. The stub keeps the advertised range actually true.Tests
Added a second vitest project (
nodeenvironment — no jsdom, noplaycanvasmocks, same conditions as an SSG build) with two regression tests:graphicsDevice.updateClientRect()— the exact probe engine 2.20+ runs at construction time.Test 2 fails without the stub on every engine version (the probe has existed since long before 2.20), so the guard doesn't depend on which engine version CI happens to install.
Verification
Import of the packed tarball in plain Node (
await import('@playcanvas/react')):getBoundingClientRect is not a functiongetBoundingClientRect is not a functionAlso verified in a minimal Next.js 16 App Router app (server page →
'use client'scene with<Application>/<Entity>/<Camera>/<Light>/<Render>) on engine 2.20.4:next buildfails with the published package and succeeds with this PR, and the built app hydrates and renders the scene in the browser with no console errors.Possible follow-up (not in this PR): a small CI matrix running the node/SSR tests against the peer-range floor and latest engine, so future engine releases can't silently break the import contract again.