Skip to content

fix!(Video): upgrade react-player to v3 and split Video/Markdown out of the main barrel - #3439

Open
dreamwasp wants to merge 16 commits into
mainfrom
cass-gmt-react-player-v3
Open

dreamwasp wants to merge 16 commits into
mainfrom
cass-gmt-react-player-v3

Conversation

@dreamwasp

@dreamwasp dreamwasp commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Upgrades react-player to v3 to drop the vulnerable deepmerge@4.3.1 (CVE-2026-93753, prototype pollution, no upstream fix), and moves Video out of the main barrel so consumers who never render it, including webpack 4 apps, aren't broken by react-player v3's dependency tree. Replaces the patch-only approach in #3438.

Dependencies

  • react-player — bumps ^2.16.0 to ^3.4.0, which removes deepmerge from the tree (yarn why deepmerge confirms)
  • @vidstack/react, react-player — now peerDependencies (and devDependencies) at ~1.12.12 and ^3.4.0; consumers who use Video install them themselves
  • @vidstack/react — pinned to ~1.12.12 because 1.14+ uses static class blocks that break Jest in repos whose Babel config can't parse them

Video

  • Video — now imported from @codecademy/gamut/Video, not the main barrel; a legacy subpath package (packages/gamut/Video/package.json, no "exports") keeps it resolvable by webpack 4. Breaking: import path change for Video consumers
  • Video — adapts to the react-player v3 API (src replaces url, YouTube color config) and drops the removed ReactPlayerWithWrapper type
  • Video — sets an accessible title on the provider iframe (videoTitle, or "Video player" as a fallback) as soon as it's inserted. react-player v3 puts title on its custom element, so the inner iframe was unnamed and failed axe frame-title (e.g. portal-app's About page e2e)

Markdown

  • Markdown — Iframe and MarkdownVideo overrides are now opt-in via iframeOverride / videoOverride, so Markdown no longer statically imports Video. Without opting in, <iframe> and <video> tags render as plain tags. Breaking: consumers relying on the polished embed must pass the overrides
  • Iframe, MarkdownVideo — re-exported from @codecademy/gamut/Video so opting in doesn't touch the main barrel

Tests

  • Video.test.tsx — the react-player mock now mimics v3 (iframe inserted after mount, no title, onReady never called); adds a case for the default title
  • Markdown.test.tsx — covers the opt-in overrides

Docs / versioning

  • Installation.mdx, Video.mdx, Markdown.mdx — document the subpath import, peer dependencies, opt-in overrides, and Jest/Babel setup for newer vidstack
  • .nx/version-plans — adds a version plan

PR Checklist

  • Related to designs: N/A
  • Related to JIRA ticket: N/A
  • Version plan added/updated
  • I have run this code to verify it works
  • This PR includes unit tests for the code change
  • This PR includes testing instructions for the code change

Testing Instructions

  1. yarn jest packages/gamut/src/Video packages/gamut/src/Markdown and yarn tsc --noEmit -p packages/gamut pass.
  2. yarn why deepmerge shows it is no longer under react-player.
  3. In Storybook, open Molecules / Video and play a YouTube and a Vimeo story. In DevTools, the player <iframe> has a title attribute and the Console has no errors.
  4. In Storybook, open Organisms / Markdown and check the video override story renders the Gamut player, and that a plain markdown <iframe> renders as a bare tag.
  5. Consumer checks against a build of this branch:
    • mono (Jest): yarn nx run ui-login-or-register:test:ci passes with @vidstack/react@~1.12.12 and react-player@^3.4.0 installed.
    • Codecademy (webpack 4): yarn build:app completes. It never imports Video directly, but @codecademy/brand does.
    • portal-app e2e: AboutPage.cy.ts passes the axe frame-title check

Linked consumer PRs:
https://github.com/codecademy-engineering/mono/pull/13776
https://github.com/codecademy-engineering/Codecademy/pull/41230

dreamwasp and others added 4 commits September 23, 2026 12:41
deepmerge@4.3.1 (transitive via react-player, used in the Video
component) has no upstream fix for a prototype pollution vuln. Patch
mergeObject's unsafe-key check to reject __proto__/constructor/prototype
keys, and pin all in-tree deepmerge ranges to the patched build via
yarn resolutions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This reverts commit 68b313c.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… dependency

react-player@2.x pulled in deepmerge@4.3.1, which has a prototype
pollution vuln (CVE-2026-93753, CVSS 8.7) with no upstream fix.
react-player@3 dropped deepmerge entirely in favor of native media
elements, so upgrading removes the vulnerable dependency from
@Codecademy/gamut's tree rather than patching it.

- url prop renamed to src; youtube config no longer nests under
  playerVars; vimeo's config.title is now a boolean (native title
  overlay toggle), not a string, so the accessible video title is
  carried solely by the existing title prop
- dropped the dead ReactPlayerWithWrapper type (v3's ref now forwards
  to the underlying HTMLVideoElement, not a wrapper element)
- react-player is ESM-only in v3, so Jest needs it in
  transformIgnorePatterns; Video.test.tsx now mocks react-player
  directly (matching the existing pattern in Markdown.test.tsx)
  instead of rendering the real Vimeo/YouTube custom elements, which
  don't mount under jsdom

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@nx-cloud

nx-cloud Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 2a7ffd6


☁️ Nx Cloud last updated this comment at 2026-10-09 19:25:39 UTC

…onsumers

react-player@3's dependency tree (tiktok-video-element, vimeo-video-element,
etc.) relies on package.json "exports" subpaths that legacy bundlers like
webpack 4 can't resolve. Because @Codecademy/gamut's main entry re-exports
every component from one flat barrel, ANY consumer resolving
'@Codecademy/gamut' was forced to resolve react-player's exports-map-only
subpaths at build time, even if they never rendered <Video> - this is what
broke Codecademy's build despite it not importing Video anywhere.

- Remove `export * from './Video'` from the main barrel; add a legacy
  subpath package (packages/gamut/Video/package.json, no "exports" field
  needed) so `@Codecademy/gamut/Video` still resolves via plain Node
  resolution. Consumers using Video need this one import path change.
- Markdown's Iframe/MarkdownVideo overrides also statically imported Video
  via a relative path, so any Markdown consumer was equally exposed
  regardless of the Video split. Made these opt-in via new
  `iframeOverride`/`videoOverride` props instead of always-on defaults;
  without opting in, <iframe>/<video> tags in markdown now render as plain
  tags rather than the polished Video component. Iframe/MarkdownVideo are
  re-exported from the Video subpath so opting in doesn't touch the main
  barrel either.
- Updated gamut's own internal Video usages (styleguide stories,
  code-connect) to the new subpath import.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dreamwasp dreamwasp changed the title fix(Video): upgrade react-player to v3 to remove vulnerable deepmerge dependency fix(Video): upgrade react-player to v3 and split Video/Markdown out of the main barrel Sep 24, 2026
…verrides

Video.mdx now points at '@Codecademy/gamut/Video' instead of the main
barrel, and explains why. Markdown.mdx gets a new Video overrides section
covering iframeOverride/videoOverride, corrects the outdated
skipDefaultOverrides claim about iframe, and adds stories/example.md
copy showing both the opted-in and bare-tag fallback states.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

⚠️ JUnit XML file not found

The CLI was unable to find any JUnit XML files to upload.
For more help, visit our troubleshooting guide.

dreamwasp and others added 6 commits October 5, 2026 12:09
Now that Video lives behind the @Codecademy/gamut/Video subpath, the main
barrel no longer imports either player library, so consumers who don't use
Video shouldn't have to install them.

- Move @vidstack/react and react-player from dependencies to optional
  peerDependencies; keep them as devDependencies for the repo's own
  build, tests, and styleguide.
- Document the install step in the Video docs and the version plan, and
  add a pointer comment at the top of src/Video/index.tsx for anyone
  following a "Can't resolve" bundler error.
@dreamwasp dreamwasp changed the title fix(Video): upgrade react-player to v3 and split Video/Markdown out of the main barrel fix!(Video): upgrade react-player to v3 and split Video/Markdown out of the main barrel Oct 7, 2026
@dreamwasp
dreamwasp marked this pull request as ready for review October 7, 2026 18:42
@dreamwasp
dreamwasp requested a review from a team as a code owner October 7, 2026 18:42
@dreamwasp
dreamwasp requested review from jakemhiller and sh0ji October 7, 2026 19:50

@LinKCoding LinKCoding left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice!
I left some comments, nothing's blocking, but I do think some points could be touched up :)

Comment thread packages/gamut/src/Markdown/index.tsx Outdated
createVideoOverride('video', {
component: MarkdownVideo,
}),
videoOverride && createVideoOverride('video', videoOverride),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure the state of markdown updates, but would rather use objects to supply arguments.
esp for this one since the function would imply the 'video' is always the tag that is being supplied to createVideoOverride (as compared to more generic createTagOverride)

it('loads a video with a vimeo URL', async () => {
const { view } = renderView({
videoUrl: 'https://vimeo.com/145702525',
videoUrl: 'https://vimeo.com/1218916076',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lol this is so funny
optionally: update videoTitle here 😂


### Installation

`Video`'s player libraries are peer dependencies of `@codecademy/gamut`, so they aren't installed automatically. They're only needed if you import from `@codecademy/gamut/Video`; the main `@codecademy/gamut` barrel never loads them, so apps that don't use `Video` can skip them. Add them alongside Gamut to use `Video` (or the Markdown `Iframe`/`MarkdownVideo` overrides):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this language can be cleaned up a bit by explicitly stating what "they" are, and seems to repeat itself a bit. Something like:

Video's player libraries are peer dependencies of @codecademy/gamut, so they aren't installed automatically. If the Video component is needed, then import from @codecademy/gamut/Video. Add the following packages alongside Gamut to use Video (or the Markdown Iframe/MarkdownVideo overrides):

yarn add @vidstack/react@~1.12.12 react-player@^3.4.0
```

Pin the versions: `@vidstack/react`'s npm `latest` tag still points to 0.6.x (1.x is published under `next`), so a bare `yarn add @vidstack/react` installs 0.6, which lacks the `@vidstack/react/player/layouts/default` subpath `Video` imports and fails with `Module not found`. Versions 1.14+ use static class blocks, which break Jest in repos whose Babel config can't parse them (see "Babel and newer syntax" on the Installation page).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe add a sample package.json for what a user should expect and do after installing vidstack and react-player?

// package.json
...
  "dependencies": {
    ...
    "@vidstack/react": "~1.12.12",
  }
  "resolutions": { 
  ... 
   "@vidstack/react": "~1.12.12",
  }
...


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is the resolution something that gamut should also do?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

i don't even think we need the resolution at all tbh, i;ll add an example though!

import { Video } from '@codecademy/gamut/Video';
```

`Video` is imported from `@codecademy/gamut/Video`, not the main `@codecademy/gamut` barrel. It depends on `react-player`, whose own dependency tree relies on package.json `"exports"` subpaths that legacy bundlers (e.g. webpack 4) can't resolve — keeping it off the main barrel means consumers who don't use `Video` never need to resolve that tree. To render video content embedded in `Markdown` text, see the [Video overrides](../Organisms/Markdown/Markdown.mdx#video-overrides) section on the Markdown docs page instead of importing `Video` directly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Video is imported from @codecademy/gamut/Video, not the main @codecademy/gamut barrel.

This repeats a lot of info in the first newly added paragraph (where imports are happening)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It depends on react-player, whose own dependency tree relies on package.json "exports" subpaths that legacy bundlers (e.g. webpack 4) can't resolve — keeping it off the main barrel means consumers who don't use Video never need to resolve that tree.

Adding a sample package.json could potentially remove the need for this explanation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Video overrides

This linking doesn't work for me :\

@codecademydev

Copy link
Copy Markdown
Collaborator

📬 Published Alpha Packages:

Package Version npm Diff
@codecademy/gamut 73.6.3-alpha.2b3b95.0 npm diff
@codecademy/gamut-icons 10.2.1-alpha.2b3b95.0 npm diff
@codecademy/gamut-illustrations 1.1.1-alpha.2b3b95.0 npm diff
@codecademy/gamut-kit 3.0.26-alpha.2b3b95.0 npm diff
@codecademy/gamut-patterns 1.1.1-alpha.2b3b95.0 npm diff
@codecademy/gamut-styles 21.2.1-alpha.2b3b95.0 npm diff
@codecademy/gamut-tests 7.1.1-alpha.2b3b95.0 npm diff
@codecademy/variance 1.1.1-alpha.2b3b95.0 npm diff
eslint-plugin-gamut 3.1.1-alpha.2b3b95.0 npm diff

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

@dreamwasp
dreamwasp requested a review from LinKCoding October 9, 2026 19:29

This branch has not been deployed

No deployments
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.

3 participants