Skip to content

fix(ssr): stop installing a window global during server rendering - #3456

Merged
kwakayama merged 1 commit into
mainfrom
fix/ssr-window-global-blocks-nonce
Aug 7, 2026
Merged

fix(ssr): stop installing a window global during server rendering#3456
kwakayama merged 1 commit into
mainfrom
fix/ssr-window-global-blocks-nonce

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What

setupSSRGlobals no longer installs a bare window global. Everything else the stub provides — document, navigator, location, matchMedia, storage, DOM constructors — stays, and the constructors remain reachable through document.defaultView.

Why

next-themes renders its anti-flash script with:

nonce={typeof window === 'undefined' ? nonce : ''}

Under veryfront SSR that ternary took the browser branch, so the script went out as <script nonce="">, and a nonce-based CSP blocked it on every render.

Confirmed against production. The blocked script's hash on codersociety's preview is sha256-Rcq79NsqIRfp7JA/RMr/1IFEj6q7YAyDVrf6BpzxXwM=, which is exactly the sha256 of that tag's contents in the served HTML. It was the only element in the document with an empty nonce — every framework-emitted script carried the real one.

typeof window === "undefined" is how the ecosystem asks whether it is on the server, and the stub answered "no". First-party code was already routed around this — isServerEnvironment() (src/platform/compat/runtime.ts:140) exists for precisely this reason and consults __VERYFRONT_SSR__ first — but a project's dependencies cannot call it, so the workaround only ever covered half the problem. Removing the global makes the question answer itself, for everyone.

Before / after

A probe evaluating next-themes' exact expression under setupSSRGlobals:

typeof window emitted nonce attr
before "object" "" → blocked
after "undefined" real nonce → allowed

Compatibility

Nothing the stub exists to provide is withdrawn. Libraries needing DOM constructors find them as bare globals; those that arrive via an element (Headless UI's focus manager reads window.HTMLElement.prototype through ownerDocument.defaultView) resolve through document.defaultView, wired to the stub.

The early-return guard is unchanged and still correct: window and document both defined means a real browser, and since this function no longer installs window, that condition can no longer be produced by an earlier call to it.

Verification

Full unit suite green: 3811 passed, 27995 steps, 0 failed. deno check, lint, fmt clean on src/rendering/ssr-globals.

Summary by CodeRabbit

  • Bug Fixes
    • Improved server-side rendering compatibility by avoiding installation of a global window.
    • Preserved access to required browser APIs and DOM constructors through supported global and document-level interfaces.
    • Added coverage to verify SSR globals are initialized correctly without browser-specific leakage.

next-themes renders its anti-flash script with
`nonce={typeof window === 'undefined' ? nonce : ''}`. Under veryfront SSR that
ternary took the browser branch, so the script went out as `<script nonce="">`,
and a nonce-based CSP blocked it on every render. Confirmed against production:
the blocked script's hash on codersociety's preview is
sha256-Rcq79NsqIRfp7JA/RMr/1IFEj6q7YAyDVrf6BpzxXwM=, which is exactly the
sha256 of that tag's contents in the served HTML. It was the only element in
the document with an empty nonce; every framework-emitted script carried the
real one.

`typeof window === "undefined"` is how the ecosystem asks whether it is on the
server, and setupSSRGlobals answered "no" by installing a stub. First-party
code was already routed around this -- `isServerEnvironment()` exists for
precisely this reason and consults `__VERYFRONT_SSR__` first -- but a project's
dependencies cannot call it, so the workaround only ever covered half the
problem. Removing the global makes the question answer itself, for everyone.

Nothing the stub exists to provide is withdrawn. `document`, `navigator`,
`location`, `matchMedia`, storage and the DOM constructors are all still
installed, and the constructors remain reachable through `document.defaultView`
for libraries that arrive via an element rather than a bare global -- the
Headless UI focus-manager path the stub was written for. Only the bare `window`
binding goes.

The early-return guard is unchanged and still correct: `window` and `document`
both defined means a real browser, and since this function no longer installs
`window`, that condition can no longer be produced by an earlier call to it.

Verified: a probe evaluating next-themes' exact nonce expression under
setupSSRGlobals emits `""` before this change and the real nonce after. Full
unit suite green -- 3811 passed, 27991 steps, 0 failed. deno check, lint and
fmt clean on src/rendering/ssr-globals.
@kojiwakayama
kojiwakayama requested a review from kwakayama as a code owner August 7, 2026 12:05
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 985cbad9-ead9-47cc-9335-8794e615d039

📥 Commits

Reviewing files that changed from the base of the PR and between 375412a and d45ddb5.

📒 Files selected for processing (2)
  • src/rendering/ssr-globals/dom-stubs.test.ts
  • src/rendering/ssr-globals/index.ts

📝 Walkthrough

Walkthrough

setupSSRGlobals() no longer installs a global window. It still exposes the SSR document, browser APIs, and DOM constructors. Tests verify access through document.defaultView and confirm the global setup behavior.

Changes

SSR global setup

Layer / File(s) Summary
SSR global setup behavior
src/rendering/ssr-globals/index.ts
setupSSRGlobals() no longer assigns the stub to global window. It preserves SSR detection, global DOM constructors, and document.defaultView access.
SSR global setup validation
src/rendering/ssr-globals/dom-stubs.test.ts
Tests use document.defaultView for observer constructors and verify that setup leaves window undefined while exposing the required SSR globals.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: kwakayama

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing SSR setup from installing a global window.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ssr-window-global-blocks-nonce

Comment @coderabbitai help to get the list of available commands.

@kwakayama
kwakayama added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 6f877fb Aug 7, 2026
31 checks passed
@kwakayama
kwakayama deleted the fix/ssr-window-global-blocks-nonce branch August 7, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants