Skip to content

fix(packages): add server-only bundles #1349

Merged
mihar-22 merged 18 commits intomainfrom
fix/ssr-safe-media-imports
Apr 16, 2026
Merged

fix(packages): add server-only bundles #1349
mihar-22 merged 18 commits intomainfrom
fix/ssr-safe-media-imports

Conversation

@mihar-22
Copy link
Copy Markdown
Member

@mihar-22 mihar-22 commented Apr 15, 2026

closes #1351 #1343

Summary

  • Server build mode (platform: 'node', __BROWSER__: 'false') for core, html, react, utils
  • browser/default export conditions so SSR resolvers get server-safe bundles
  • __BROWSER__ compile-time flag replaces runtime typeof window / globalThis.document checks
  • Co-located browser.ts + server.ts per media directory — server stubs are no-op classes
  • ReactiveElement extends globalThis.HTMLElement ?? class {} for SSR-safe module evaluation
  • stubDefinePlugin emits stub classes so preset re-exports resolve on server
  • pnpm check:ssr script validates all 99 server entry points import cleanly in Node.js
  • SSR check runs in CI as part of the build job

Packages changed

Package Changes
@videojs/core browser.ts/server.ts per media dir, 3 build modes, browser/default exports, glob-based media entry discovery
@videojs/html Server build mode, CSS + define stub plugins, browser/default exports, safeDefine uses __BROWSER__
@videojs/react Server build mode, browser/default exports
@videojs/utils Server build mode, browser condition on ./dom, __BROWSER__ guards
@videojs/element ReactiveElement SSR-safe base class fallback
CI pnpm check:ssr added to build job in ci.yml

Test plan

  • pnpm build:packages — all packages build with dist/server/ output
  • pnpm typecheck — passes
  • pnpm test — all 22 test tasks pass
  • pnpm check:workspace — 7/7
  • pnpm check:ssr — 99/99 server entry points import successfully

🤖 Generated with Claude Code


Note

Medium Risk
Touches build pipelines and exports resolution across multiple published packages; misconfiguration could break consumer bundling/SSR imports despite minimal runtime logic changes.

Overview
Adds a server build mode (platform: 'node', __BROWSER__ define, dist/server/ outputs) across key packages and updates package exports to use browser/default condition splits so SSR resolvers pick server-safe entry points.

Introduces SSR-focused tooling and guardrails: a new pnpm check:ssr script (.github/scripts/ssr-check.js) that imports all server entries and is enforced in CI, plus a check:workspace rule that validates browser exports match tsdown server-build configuration.

Refactors DOM-only code paths to be compile-time gated via __BROWSER__ (replacing runtime global checks), adds per-media-module browser.ts/server.ts stubs in @videojs/core, and adjusts HTML/React build configs (including HTML stubbing plugins) and tests to target browser entry points.

Reviewed by Cursor Bugbot for commit b5d9b03. Bugbot is set up for automated code reviews on this repo. Configure here.

…a imports

Add `__BROWSER__` compile-time flag and server build modes across the
package chain so SSR resolvers get server-safe bundles without
browser-only library imports (hls.js, dashjs, mux-embed).

Each media directory now has `browser.ts` (full impl) and `server.ts`
(SSR-safe stub). Package exports use `browser`/`default` conditions
to route imports to the correct bundle.

Closes #1343

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Apr 16, 2026 7:51am

Request Review

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 15, 2026

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit b5d9b03
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/69e0950226935f000861ef5b
😎 Deploy Preview https://deploy-preview-1349--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Size
/video (default) 27.82 kB
/video (default + hls) 159.88 kB
/video (minimal) 25.36 kB
/video (minimal + hls) 157.43 kB
/audio (default) 25.89 kB
/audio (minimal) 23.50 kB
/background 4.15 kB
Media (8)
Entry Size
/media/background-video 1.03 kB
/media/container 1.72 kB
/media/dash-video 236.27 kB
/media/hls-video 133.31 kB
/media/mux-audio 155.92 kB
/media/mux-video 155.81 kB
/media/native-hls-video 3.42 kB
/media/simple-hls-video 15.68 kB
Players (3)
Entry Size
/video/player 6.74 kB
/audio/player 5.07 kB
/background/player 3.87 kB
Skins (17)
Entry Type Size
/video/minimal-skin.css css 3.47 kB
/video/skin.css css 3.49 kB
/video/minimal-skin js 25.33 kB
/video/minimal-skin.tailwind js 25.54 kB
/video/skin js 27.80 kB
/video/skin.tailwind js 27.94 kB
/audio/minimal-skin.css css 2.53 kB
/audio/skin.css css 2.50 kB
/audio/minimal-skin js 23.46 kB
/audio/minimal-skin.tailwind js 23.65 kB
/audio/skin js 25.86 kB
/audio/skin.tailwind js 25.96 kB
/background/skin.css css 117 B
/background/skin js 1.14 kB
/base.css css 157 B
/shared.css css 88 B
/skin-element js 1.34 kB
UI Components (24)
Entry Size
/ui/alert-dialog 1.02 kB
/ui/alert-dialog-close 477 B
/ui/alert-dialog-description 394 B
/ui/alert-dialog-title 401 B
/ui/buffering-indicator 2.18 kB
/ui/captions-button 2.45 kB
/ui/compounds 3.89 kB
/ui/controls 2.07 kB
/ui/error-dialog 2.64 kB
/ui/fullscreen-button 2.46 kB
/ui/hotkey 1.74 kB
/ui/mute-button 2.44 kB
/ui/pip-button 2.43 kB
/ui/play-button 2.44 kB
/ui/playback-rate-button 2.43 kB
/ui/popover 1.90 kB
/ui/poster 1.99 kB
/ui/seek-button 2.48 kB
/ui/slider 1.53 kB
/ui/thumbnail 2.59 kB
/ui/time 2.29 kB
/ui/time-slider 3.66 kB
/ui/tooltip 2.02 kB
/ui/volume-slider 2.49 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Size
/video (default) 22.61 kB
/video (default + hls) 153.38 kB
/video (minimal) 20.30 kB
/video (minimal + hls) 151.35 kB
/audio (default) 18.59 kB
/audio (minimal) 17.12 kB
/background 754 B
Media (7)
Entry Size
/media/background-video 575 B
/media/dash-video 234.92 kB
/media/hls-video 131.92 kB
/media/mux-audio 154.59 kB
/media/mux-video 154.67 kB
/media/native-hls-video 1.98 kB
/media/simple-hls-video 14.38 kB
Skins (14)
Entry Type Size
/video/minimal-skin.css css 3.40 kB
/video/skin.css css 3.42 kB
/video/minimal-skin js 20.24 kB
/video/minimal-skin.tailwind js 23.71 kB
/video/skin js 22.54 kB
/video/skin.tailwind js 23.76 kB
/audio/minimal-skin.css css 2.43 kB
/audio/skin.css css 2.39 kB
/audio/minimal-skin js 17.03 kB
/audio/minimal-skin.tailwind js 19.50 kB
/audio/skin js 18.50 kB
/audio/skin.tailwind js 19.52 kB
/background/skin.css css 90 B
/background/skin js 272 B
UI Components (19)
Entry Size
/ui/alert-dialog 1.08 kB
/ui/buffering-indicator 1.15 kB
/ui/captions-button 1.84 kB
/ui/controls 1.23 kB
/ui/error-dialog 1.67 kB
/ui/fullscreen-button 1.85 kB
/ui/mute-button 1.84 kB
/ui/pip-button 1.86 kB
/ui/play-button 1.82 kB
/ui/playback-rate-button 1.83 kB
/ui/popover 1.88 kB
/ui/poster 1.12 kB
/ui/seek-button 1.83 kB
/ui/slider 2.53 kB
/ui/thumbnail 1.43 kB
/ui/time 1.27 kB
/ui/time-slider 2.03 kB
/ui/tooltip 2.18 kB
/ui/volume-slider 2.08 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core

Path Base PR Diff %
/dom/media/media-host 528 B 🆕
/dom/media/predicate 303 B 🆕
/dom/media/video-host 636 B 🆕
Entries (11)
Entry Size
. 4.79 kB
/dom 11.31 kB
/dom/media/custom-media-element 1.90 kB
/dom/media/dash 234.26 kB
/dom/media/hls 131.33 kB
/dom/media/media-host 528 B
/dom/media/mux 153.96 kB
/dom/media/native-hls 1.24 kB
/dom/media/predicate 303 B
/dom/media/simple-hls 13.65 kB
/dom/media/video-host 636 B
🏷️ @videojs/element — no changes
Entries (2)
Entry Size
. 1012 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Size
. 1.39 kB
/html 695 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Size
/array 104 B
/dom 1.76 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 148 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (3)
Entry Size
. 40 B
/dom 13.30 kB
/playback-engine 13.17 kB

ℹ️ How to interpret

All sizes are standalone totals (minified + brotli).

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

@mihar-22 mihar-22 changed the title fix(core,html,react,utils): server-only bundles for SSR-safe media imports fix(packages): server-only bundles for SSR-safe media imports Apr 16, 2026
@mihar-22 mihar-22 changed the title fix(packages): server-only bundles for SSR-safe media imports fix(packages): add server-only bundles Apr 16, 2026
Validates that packages with `browser` export conditions have matching
server build configuration in tsdown — `'server'` build mode,
`__BROWSER__` define, and `platform: 'node'`.

Also checks that `default` export paths point to `dist/server/`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract shared HLS types (PreloadType, PlaybackType, SourceType,
  PlaybackTypes, SourceTypes, inferSourceType) into hls/types.ts
- Import shared types in both browser.ts and server.ts instead of
  redeclaring
- Replace `export * from './browser'` in native-hls, simple-hls, and
  custom-media-element server stubs with explicit no-op classes
- Add SSR safety tests for NativeHlsMedia, SimpleHlsMedia, and
  CustomMediaElement server stubs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All six media subpath exports now share the same browser/default
condition shape, so collapse them into a single `./dom/media/*`
wildcard export.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server stubs only need to not crash on import and construction.
Strip them down to bare minimum: public fields for settable props,
engine = null, destroy calls detach. No 1:1 mirroring of browser API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CSS custom properties are irrelevant on the server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Simplify HLS type re-exports to `export * from './types'`
- Replace `typeof window`/`document` checks with `__BROWSER__` in
  mux-data.ts and dismiss-layer.ts
- Add edge-runtime SSR safety tests across core, html, and react
  packages using @edge-runtime/vm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add serverMethodError helper — server stubs throw clear errors when
  load()/destroy() are called instead of silently no-oping.
- Set __DEV__: true for server builds (SSR has no prod mode).
- Rewrite edge SSR tests to iterate package.json exports via bare
  package specifiers instead of importing from dist/.
- Fix bundle-size.js to resolve browser.default (prod bundle) over
  top-level default (server bundle), and add conditions: ['browser']
  to esbuild build() calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bare package specifiers (e.g. `import('@videojs/react/video')`) fail
in pnpm workspaces because packages don't have self-referencing
symlinks. Resolve the server export paths from package.json and import
via file:// URLs instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ts from edge tests

Rename `serverMethodError(className, method)` to generic
`serverError(label)` taking a single string like `HlsMedia.destroy()`.

Remove html and react edge SSR tests — they only tested dist imports
which src tests should never do. Core keeps its source-based server
stub tests in edge-runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mihar-22 and others added 2 commits April 16, 2026 00:19
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a post-build script (.github/scripts/ssr-check.js) that auto-discovers
all package exports with browser/default condition splits, resolves the
server (default) path, and dynamically imports each one in Node.js. Any
module that throws during evaluation fails the check.

- Add `pnpm check:ssr` root script and CI step in the build job
- Fix ReactiveElement to extend `globalThis.HTMLElement ?? class {}` so
  the class definition evaluates safely on the server
- Fix html stubDefinePlugin to emit stub classes (not just `export {}`)
  so preset modules can re-export skin element names
- Remove per-package edge-runtime vitest tests (superseded by script)
- 99/99 server entry points now import successfully

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mihar-22 mihar-22 marked this pull request as ready for review April 16, 2026 07:58
@mihar-22 mihar-22 merged commit 3331fda into main Apr 16, 2026
25 checks passed
@mihar-22 mihar-22 deleted the fix/ssr-safe-media-imports branch April 16, 2026 07:59
@luwes luwes mentioned this pull request Apr 16, 2026
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b5d9b03. Configure here.

if (typeof exportValue === 'object' && exportValue !== null) {
if (typeof exportValue.browser === 'object' && exportValue.browser !== null) {
return exportValue.browser.default ?? exportValue.default ?? null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bundle size measures server bundle for string browser exports

Medium Severity

resolveDefault only handles browser when it's a nested object (e.g. { browser: { default: "..." } }), but @videojs/utils's ./dom export uses a plain string: { browser: "./dist/dom.js", default: "./dist/server/dom.js" }. Since typeof exportValue.browser === 'object' is false for strings, it falls through to exportValue.default, returning the server bundle path. This causes the bundle-size script to measure the server (no-op) bundle instead of the actual browser bundle for @videojs/utils/dom.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b5d9b03. Configure here.

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.

Feature: Server-Only Bundles with browser/default Export Conditions

1 participant