<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abdullah Al Fahad</title>
    <description>The latest articles on DEV Community by Abdullah Al Fahad (@imfahad).</description>
    <link>https://dev.to/imfahad</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F245020%2F09ecdc39-71a3-4bc3-aa60-316734281d96.png</url>
      <title>DEV Community: Abdullah Al Fahad</title>
      <link>https://dev.to/imfahad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/imfahad"/>
    <language>en</language>
    <item>
      <title>I built a remote desktop for my Macs with no account, no cloud and no open ports</title>
      <dc:creator>Abdullah Al Fahad</dc:creator>
      <pubDate>Fri, 02 Oct 2026 21:37:00 +0000</pubDate>
      <link>https://dev.to/imfahad/i-built-a-remote-desktop-for-my-macs-with-no-account-no-cloud-and-no-open-ports-537m</link>
      <guid>https://dev.to/imfahad/i-built-a-remote-desktop-for-my-macs-with-no-account-no-cloud-and-no-open-ports-537m</guid>
      <description>&lt;p&gt;I wanted to reach my own Macs from my phone and my laptop. Every tool I tried wanted an account,&lt;br&gt;
sent my sessions through someone else's servers, or both. For machines that hold everything I&lt;br&gt;
work on, that felt like the wrong trade.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;OwnDesk&lt;/strong&gt;: a Mac hosts, and another Mac, an Android phone or an iPhone controls it.&lt;br&gt;
There is no account, no cloud service and no port open to the Internet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foaqxdcchmh62wmvncxvi.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foaqxdcchmh62wmvncxvi.gif" alt="A phone controlling a Mac: tapping into a note, typing, zooming in, trackpad mode" width="720" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is open source under the MIT license, and v0.4.0 has a Mac download and an Android APK:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/im-fahad/OwnDesk" rel="noopener noreferrer"&gt;github.com/im-fahad/OwnDesk&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is about the design decisions, because they are the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  No server, so who connects the two devices?
&lt;/h2&gt;

&lt;p&gt;On the same network, nobody needs to. The Mac advertises itself over Bonjour, and the controller&lt;br&gt;
connects straight to a WebSocket the Mac serves.&lt;/p&gt;

&lt;p&gt;Away from home, I use &lt;a href="https://tailscale.com" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt;. The Mac has a stable tailnet address,&lt;br&gt;
Tailscale connects the two devices directly when it can, and relays when it cannot. OwnDesk never&lt;br&gt;
runs a server of its own.&lt;/p&gt;

&lt;p&gt;One detail made this work in practice. A Mac announces its Tailscale addresses in its Bonjour&lt;br&gt;
record, and every paired device that hears it at home keeps them. So a phone finds the Mac from a&lt;br&gt;
cafe even if the two were paired while Tailscale was off. The controller probes every address it&lt;br&gt;
knows at once and uses the first one that answers, so the same button works at home and away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust comes from one moment: pairing
&lt;/h2&gt;

&lt;p&gt;Pairing happens once, face to face, on the same network:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Mac shows a QR code holding its device id, its addresses and a one-time pairing code.&lt;/li&gt;
&lt;li&gt;The phone scans it and sends a pairing request with an HMAC proving it has the code, without
sending the code itself.&lt;/li&gt;
&lt;li&gt;Both screens show a short fingerprint, like &lt;code&gt;25AA-F3B7-4F82&lt;/code&gt;. If they match, you click Approve
on the Mac.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That comparison is the whole security of pairing. After it, each side stores the other's public key,&lt;br&gt;
and that key is the only thing that can ever start a session.&lt;/p&gt;

&lt;p&gt;The keys themselves never leave hardware. On a Mac and an iPhone they live in the Secure Enclave,&lt;br&gt;
and on Android in the Keystore. Both support ECDSA P-256 and neither supports Ed25519, which&lt;br&gt;
decided the curve. A device's id is simply the SHA-256 of its public key, so the id proves which key&lt;br&gt;
it belongs to and nothing has to be registered anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing the session, not trusting the network
&lt;/h2&gt;

&lt;p&gt;Every message that sets up a session is a signed envelope, checked in a fixed order: size, version,&lt;br&gt;
recipient, known sender, signature, clock skew, replay, and finally the JSON Schema. Any failure is&lt;br&gt;
final, and most failures get no reply at all.&lt;/p&gt;

&lt;p&gt;The part I like most is how this protects the video itself. The stream is WebRTC, encrypted with&lt;br&gt;
DTLS-SRTP. Normally the weak point is the SDP exchange: if someone in the middle swaps the DTLS&lt;br&gt;
fingerprint in the SDP, they can sit in the middle of the encrypted stream. In OwnDesk the SDP&lt;br&gt;
travels inside a signed envelope, so the DTLS fingerprint is signed too. Swap it, and the signature&lt;br&gt;
fails. Neither the network nor a relay can impersonate a device or read the session.&lt;/p&gt;

&lt;h2&gt;
  
  
  One protocol, three languages
&lt;/h2&gt;

&lt;p&gt;The Mac apps are Swift, the Android app is Kotlin, and there is a TypeScript reference&lt;br&gt;
implementation. Three implementations of a security protocol is three chances to disagree.&lt;/p&gt;

&lt;p&gt;So the protocol is defined once, as JSON Schema. Codegen produces the TypeScript types and the&lt;br&gt;
Swift key table from it, and a set of shared test vectors pins down the exact bytes an envelope&lt;br&gt;
signs, the pairing proof and sixteen receiver cases covering replay, tampering, clock skew and&lt;br&gt;
unknown senders. All three implementations run the same vectors. If the Android app disagrees with&lt;br&gt;
a vector, it disagrees with both Macs, and the test says so.&lt;/p&gt;

&lt;p&gt;The iPhone app has no protocol code at all: it links the Mac controller's own core, so pairing,&lt;br&gt;
signing and reconnection are the same code a MacBook runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A terminal, without a "run command" message
&lt;/h2&gt;

&lt;p&gt;I also wanted a shell on the Mac, the way Termius gives you one. The obvious way would be a message&lt;br&gt;
type that runs commands, and that is exactly what I refused to add. The protocol has no message that&lt;br&gt;
can run a command or touch a file. There is a list in the spec of messages that must never exist.&lt;/p&gt;

&lt;p&gt;Instead, the terminal is the Mac's own SSH server with OwnDesk as the client. Each device has a&lt;br&gt;
second hardware-backed key just for SSH. The only thing OwnDesk carries for the terminal is a&lt;br&gt;
one-time request: "please let this key in". The Mac shows who is asking, someone clicks Allow, and&lt;br&gt;
the Mac writes the key line itself, tagged with the device, then answers with its host keys so the&lt;br&gt;
device can pin them. Unpairing removes the line again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatc41vdampe4et917l3d.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatc41vdampe4et917l3d.gif" alt="The phone asks the Mac for terminal access, then runs commands and selects, copies and pastes a line" width="540" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Clipboard sync, opt-in on both sides
&lt;/h2&gt;

&lt;p&gt;Clipboard sync is useful and dangerous: whatever anyone copies on the Mac, a password included,&lt;br&gt;
could reach a device. So it is off until the person at the device switches it on, and a Mac shares&lt;br&gt;
its own clipboard only with the devices it is told to, a per-device switch that starts off. What a&lt;br&gt;
device copies reaches the Mac without that switch, since it could type the same text anyway. And&lt;br&gt;
clipboard text is never logged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that cost me time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS ties permissions to the code signature.&lt;/strong&gt; I sign without an Apple certificate, and an ad-hoc
signature changes on every build, so Screen Recording and Accessibility had to be granted again
after every rebuild. The install script now clears the stale entries so the new build can ask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-size desktops fell back to VP8.&lt;/strong&gt; At 1920x1200 the H.264 level offered by default is too
low for the frame size, so the controller's offer now puts H.264 first and states level 5.2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phones only read the clipboard when they are in front.&lt;/strong&gt; On Android and iOS, clipboard sync sends
what you copied when you come back to the app, not the moment you copy it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing on real devices is where the bugs were.&lt;/strong&gt; Unit tests passed while a scripted drag on a real
phone was landing one row too low. Every feature now has an end-to-end script that drives the real
app against a real host.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it falls short
&lt;/h2&gt;

&lt;p&gt;It is early, and I would rather say so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only Macs can be controlled. Phones only control.&lt;/li&gt;
&lt;li&gt;No file transfer or audio yet.&lt;/li&gt;
&lt;li&gt;The Mac app is signed without an Apple certificate, so you confirm it once after downloading.&lt;/li&gt;
&lt;li&gt;The iPhone app must be built from source with your own Apple ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Download: &lt;a href="https://github.com/im-fahad/OwnDesk/releases" rel="noopener noreferrer"&gt;github.com/im-fahad/OwnDesk/releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The design and threat model: &lt;code&gt;docs/spec.md&lt;/code&gt; in the repository&lt;/li&gt;
&lt;li&gt;Issues and pull requests are welcome.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would especially like feedback on the security design. If you find a hole in it, please report it&lt;br&gt;
privately through GitHub's security advisories rather than in public.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>macos</category>
      <category>webrtc</category>
      <category>security</category>
    </item>
    <item>
      <title>I shipped a video player to npm — twice. What I learned about scoped CSS, "use client", and Nuxt modules.</title>
      <dc:creator>Abdullah Al Fahad</dc:creator>
      <pubDate>Sat, 09 May 2026 10:28:33 +0000</pubDate>
      <link>https://dev.to/imfahad/i-shipped-a-video-player-to-npm-twice-what-i-learned-about-scoped-css-use-client-and-nuxt-2dfl</link>
      <guid>https://dev.to/imfahad/i-shipped-a-video-player-to-npm-twice-what-i-learned-about-scoped-css-use-client-and-nuxt-2dfl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A small, HLS-capable video player for React &lt;strong&gt;and&lt;/strong&gt; Vue, with zero global CSS side-effects. Built in the open. Try it: &lt;a href="https://video-player-playgraound.vercel.app/" rel="noopener noreferrer"&gt;https://video-player-playgraound.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The annoying gap
&lt;/h2&gt;

&lt;p&gt;I needed an embeddable video player for a side project. Requirements were boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HLS streaming (&lt;code&gt;.m3u8&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A clean play button overlay, optional close button, optional desktop/mobile aspect-ratio toggle&lt;/li&gt;
&lt;li&gt;Fits the design system without fighting it&lt;/li&gt;
&lt;li&gt;Works in &lt;strong&gt;Next.js App Router&lt;/strong&gt; without a &lt;em&gt;"ReactServerComponentsError"&lt;/em&gt; headache&lt;/li&gt;
&lt;li&gt;Works in &lt;strong&gt;Nuxt 3&lt;/strong&gt; without manually wiring a plugin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried the popular options. Each was &lt;em&gt;almost&lt;/em&gt; right.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;react-player&lt;/code&gt; is mature but the API is geared toward "give me a URL and a giant control bar."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;video.js&lt;/code&gt; is overkill for an embed and ships a chunk of theme CSS that fights Tailwind.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;plyr&lt;/code&gt;, &lt;code&gt;vidstack&lt;/code&gt; — beautiful, but either too heavy or too opinionated about styling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thing that kept biting me was &lt;strong&gt;CSS bleed&lt;/strong&gt;. Every "drop-in" player I tried shipped global resets, theme tokens, or &lt;code&gt;*&lt;/code&gt; selectors that quietly nudged my buttons by 1px or rewrote my form input borders. In a design system you've spent weeks tuning, that's a paper cut you don't want.&lt;/p&gt;

&lt;p&gt;So I built one. Then, because half the consumers I had in mind were on Vue, I built it twice.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@glitchlab/react-video-player&lt;/code&gt;&lt;/strong&gt; — &lt;a href="https://www.npmjs.com/package/@glitchlab/react-video-player" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@glitchlab/react-video-player&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@glitchlab/vue-video-player&lt;/code&gt;&lt;/strong&gt; — &lt;a href="https://www.npmjs.com/package/@glitchlab/vue-video-player" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@glitchlab/vue-video-player&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are at v1.0.2 today. Both are MIT. Both have the same prop surface, same UI, and ship under 4 KB gzipped of CSS + JS.&lt;/p&gt;

&lt;p&gt;There's a live playground on Vercel where you can drop in a file or paste an HLS URL and try it end-to-end:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://video-player-playgraound.vercel.app/" rel="noopener noreferrer"&gt;https://video-player-playgraound.vercel.app/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is the build-in-public version of how I got there. Three decisions ended up doing most of the work — the rest was just plumbing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 1: kill the global CSS
&lt;/h2&gt;

&lt;p&gt;The first version of this player used Tailwind v4. I wrote the components, ran &lt;code&gt;vite build --lib&lt;/code&gt;, and the resulting &lt;code&gt;dist/style.css&lt;/code&gt; was &lt;strong&gt;17 KB&lt;/strong&gt; of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;:host&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-violet-700&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
  &lt;span class="py"&gt;--font-sans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ui-sans-serif&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;*,&lt;/span&gt; &lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;::after&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;::backdrop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--tw-translate-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire Tailwind preflight, baked in. Anyone who imported &lt;code&gt;@glitchlab/react-video-player/style.css&lt;/code&gt; would get my theme tokens injected into &lt;code&gt;:root&lt;/code&gt; and global resets on every element on their page.&lt;/p&gt;

&lt;p&gt;For an internal app this is whatever. For a published library it's malpractice. So I rewrote the CSS by hand.&lt;/p&gt;

&lt;p&gt;The full stylesheet now scopes everything under a single class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.gvp-root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;23&lt;/span&gt; &lt;span class="m"&gt;23&lt;/span&gt; &lt;span class="m"&gt;23&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.gvp-root&lt;/span&gt; &lt;span class="o"&gt;*,&lt;/span&gt;
&lt;span class="nc"&gt;.gvp-root&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.gvp-root&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;box-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.gvp-play&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;91&lt;/span&gt; &lt;span class="m"&gt;33&lt;/span&gt; &lt;span class="m"&gt;182&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;:root&lt;/code&gt;, no &lt;code&gt;*&lt;/code&gt; selectors at the document level. The component owns its subtree and nothing else. The CSS file dropped from &lt;strong&gt;17 KB to 2.8 KB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The bonus: consumers can now override with predictable specificity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.gvp-root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.gvp-play&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rebeccapurple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.gvp-toggle-btn.is-active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;deeppink&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole "design system" story. No &lt;code&gt;@apply&lt;/code&gt;, no &lt;code&gt;:where()&lt;/code&gt; tricks, no Tailwind dependency in the lib. Tailwind users still get to use Tailwind in their own app — the player just stops yelling at theirs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; if you ship CSS in an npm package, treat it like API. Every selector you publish is a contract. &lt;code&gt;:root { --color-foo: ...; }&lt;/code&gt; is a worse breaking change than removing a prop, because it breaks silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 2: preserve "use client" through the build
&lt;/h2&gt;

&lt;p&gt;The React package targets Next.js App Router. That means every file that uses hooks needs &lt;code&gt;"use client";&lt;/code&gt; at the top, otherwise Next refuses to render it from a server component.&lt;/p&gt;

&lt;p&gt;The component source had it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's the gotcha: &lt;strong&gt;rollup strips top-of-file directives during bundling unless you tell it not to.&lt;/strong&gt; I ran &lt;code&gt;vite build --lib&lt;/code&gt;, looked at &lt;code&gt;dist/index.mjs&lt;/code&gt;, and the directive was gone. Importing the package from a Next App Router server component blew up with the classic &lt;em&gt;"You're importing a component that needs &lt;code&gt;useState&lt;/code&gt;"&lt;/em&gt; error.&lt;/p&gt;

&lt;p&gt;The fix is a tiny rollup output plugin that re-prepends the directive after bundling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.ts (excerpt)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;preserveUseClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;preserve-use-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;renderChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"use client"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;'use client'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`"use client";\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;preserveUseClient&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, both &lt;code&gt;dist/index.mjs&lt;/code&gt; and &lt;code&gt;dist/index.cjs&lt;/code&gt; start with &lt;code&gt;"use client";&lt;/code&gt;. Consumers can import the package directly from a server component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/page.tsx — server component&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReactVideoPlayer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/react-video-player&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/react-video-player/style.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactVideoPlayer&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/videos/hero.m3u8"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No client-component wrapper required. Just import and render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; if your README claims "Next.js App Router compatible," verify the directive survives the bundler. Open &lt;code&gt;dist/index.mjs&lt;/code&gt; after a build. If line 1 isn't &lt;code&gt;"use client";&lt;/code&gt;, you're shipping a footgun.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 3: ship a real Nuxt module
&lt;/h2&gt;

&lt;p&gt;The Vue package was supposed to mirror the React one's "drop in and go" feel. For Nuxt, that meant a real module — not a "import this component manually in every page."&lt;/p&gt;

&lt;p&gt;Nuxt's module system is a small library of helpers (&lt;code&gt;@nuxt/kit&lt;/code&gt;) that lets you add plugins, components, and composables to a Nuxt app. The trick is that &lt;code&gt;@nuxt/kit&lt;/code&gt; imports Node-only modules (&lt;code&gt;giget&lt;/code&gt;, &lt;code&gt;node:fs&lt;/code&gt;, etc.) — so if you naively re-export your Nuxt module from your main entry, the lib's vanilla Vue users get a bundle that tries to require &lt;code&gt;node:fs&lt;/code&gt; in the browser.&lt;/p&gt;

&lt;p&gt;The fix is to give Nuxt its own subpath export and never let it touch the main entry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// package.json (excerpt)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.cjs"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./style.css"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/style.css"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./nuxt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/nuxt-module.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/nuxt-module.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/nuxt-module.cjs"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The vite config emits two entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;lib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nuxt-module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/utils/nuxt-module.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;es&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hls.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nuxt/kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/^node:.*/&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Nuxt module itself is mostly boilerplate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/utils/nuxt-module.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createResolver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defineNuxtModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nuxt/kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/vue-video-player&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;configKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vueVideoPlayer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;compatibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nuxt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;=3.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_nuxt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;addPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./nuxt-plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the plugin auto-registers the component globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/utils/nuxt-plugin.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineNuxtPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;VideoPlayer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../VideoPlayer.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtPlugin&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;nuxtApp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;nuxtApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vueApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VueVideoPlayer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;VideoPlayer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Nuxt 3 users get one-line integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// nuxt.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/vue-video-player/nuxt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/vue-video-player/style.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the component is available globally — no import, just &lt;code&gt;&amp;lt;VueVideoPlayer src="..." /&amp;gt;&lt;/code&gt; anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; subpath exports aren't optional for libraries with framework-specific entry points. Putting a Nuxt module behind &lt;code&gt;./nuxt&lt;/code&gt; means non-Nuxt consumers never load &lt;code&gt;@nuxt/kit&lt;/code&gt; and its Node-only deps. Their browser bundle stays clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  A bug that took an hour to find: Vue HLS first-mount race
&lt;/h2&gt;

&lt;p&gt;I want to call this one out because it's a category of bug that's easy to ship and almost impossible to catch with smoke tests.&lt;/p&gt;

&lt;p&gt;The Vue HLS player initialized like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videoEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLVideoElement&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;initPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;immediate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// 👈 the bug&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine. Tests passed. The deployed playground showed the player frame, the play button, the native controls bar — but &lt;strong&gt;the video never loaded&lt;/strong&gt;. Click play, the play button vanished (so &lt;code&gt;play()&lt;/code&gt; resolved), but no pixels.&lt;/p&gt;

&lt;p&gt;The cause: &lt;code&gt;watch(..., { immediate: true })&lt;/code&gt; fires the callback during &lt;code&gt;setup()&lt;/code&gt;, &lt;strong&gt;before the template renders&lt;/strong&gt;. At that point &lt;code&gt;videoEl.value&lt;/code&gt; is still &lt;code&gt;null&lt;/code&gt;. &lt;code&gt;initPlayer()&lt;/code&gt; early-returns, the watcher's already fired, and &lt;code&gt;props.src&lt;/code&gt; doesn't change again — so HLS never attaches.&lt;/p&gt;

&lt;p&gt;The React side wasn't affected because React effects always run &lt;em&gt;after&lt;/em&gt; commit. The ref is bound by the time the effect fires.&lt;/p&gt;

&lt;p&gt;The fix is two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;onMounted&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;initPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;initPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;  &lt;span class="c1"&gt;// no `immediate`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the first init runs in &lt;code&gt;onMounted&lt;/code&gt; (template ref is bound), and subsequent &lt;code&gt;src&lt;/code&gt; changes are handled by the regular watcher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; &lt;code&gt;immediate: true&lt;/code&gt; and refs don't mix cleanly in Composition API. If your watcher needs a ref that's bound by the template, use &lt;code&gt;onMounted&lt;/code&gt; for the first run and a non-immediate watch for updates.&lt;/p&gt;




&lt;h2&gt;
  
  
  The playground
&lt;/h2&gt;

&lt;p&gt;I wanted a proof point that wasn't "trust the README." So I built a minimal Next.js app with three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A drag-and-drop file zone (uploads create blob URLs and feed them to the player)&lt;/li&gt;
&lt;li&gt;A URL input with validation (paste any HTTP/HTTPS link, including &lt;code&gt;.m3u8&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The player itself, configured to remount cleanly on source change via &lt;code&gt;key={videoSrc}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's deployed at &lt;a href="https://video-player-playgraound.vercel.app/" rel="noopener noreferrer"&gt;https://video-player-playgraound.vercel.app/&lt;/a&gt;. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paste the test HLS stream &lt;code&gt;https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8&lt;/code&gt; and watch it play&lt;/li&gt;
&lt;li&gt;Paste a CORS-permissive &lt;code&gt;.mp4&lt;/code&gt; URL and watch it play&lt;/li&gt;
&lt;li&gt;Drop a local file and watch the blob URL feed the player&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It runs on the actual published &lt;code&gt;@glitchlab/react-video-player&lt;/code&gt; from npm. Every time I publish a new version, I bump the dep and redeploy — the playground exercises the real published bundle, not source.&lt;/p&gt;

&lt;p&gt;The Vue package shares the same UX in the local playground at &lt;code&gt;playground/vue/&lt;/code&gt; in the monorepo. The deployed demo is React because that's what Vercel templates make easy; the Vue version is one &lt;code&gt;pnpm dev:vue&lt;/code&gt; away if you clone the repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in v1.0.2
&lt;/h2&gt;

&lt;p&gt;Both packages at v1.0.2 today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same prop surface across React and Vue.&lt;/strong&gt; &lt;code&gt;src&lt;/code&gt;, &lt;code&gt;poster&lt;/code&gt;, &lt;code&gt;showDeviceToggle&lt;/code&gt;, &lt;code&gt;defaultDevice&lt;/code&gt;, &lt;code&gt;hoverPlay&lt;/code&gt;, &lt;code&gt;tooltipText&lt;/code&gt;, &lt;code&gt;muted&lt;/code&gt;, &lt;code&gt;loop&lt;/code&gt;, &lt;code&gt;controls&lt;/code&gt;, &lt;code&gt;frameMaxWidth&lt;/code&gt;, &lt;code&gt;aspectRatio&lt;/code&gt;, &lt;code&gt;hlsConfig&lt;/code&gt;, &lt;code&gt;isHls&lt;/code&gt;. React adds &lt;code&gt;onClose&lt;/code&gt; + &lt;code&gt;children&lt;/code&gt;; Vue uses &lt;code&gt;closable&lt;/code&gt; + &lt;code&gt;@close&lt;/code&gt; event + default slot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HLS via &lt;code&gt;hls.js&lt;/code&gt;&lt;/strong&gt; with automatic native fallback for Safari (no &lt;code&gt;hls.js&lt;/code&gt; cost when MSE isn't needed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Captions/subtitles passthrough.&lt;/strong&gt; Pass &lt;code&gt;&amp;lt;track&amp;gt;&lt;/code&gt; elements as React children or Vue default slot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hover-play with race-safe play/pause.&lt;/strong&gt; Tracks the play promise so a quick mouse-leave can't trigger a &lt;code&gt;DOMException&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hlsConfig&lt;/code&gt; is memoizable.&lt;/strong&gt; Pass a stable reference (&lt;code&gt;useMemo&lt;/code&gt; / &lt;code&gt;shallowRef&lt;/code&gt;) to avoid HLS rebuilds on render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript types&lt;/strong&gt;, full source maps, MIT licensed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smoke tests&lt;/strong&gt; in vitest (8 React, 7 Vue).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bundle size: ~3 KB JS gzipped + ~1 KB CSS gzipped, per package.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's not in v1.0.2 (yet)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Picture-in-Picture toggle&lt;/strong&gt; — the API is trivial, the UI affordance isn't. Holding off until I know how it should look in the toggle pill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playback speed control&lt;/strong&gt; — same. Will land when I find a non-busy way to expose it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio language switcher&lt;/strong&gt; — for HLS streams with multiple audio renditions (dubs, descriptive audio, secondary languages). &lt;code&gt;hls.js&lt;/code&gt; exposes &lt;code&gt;audioTracks&lt;/code&gt; and &lt;code&gt;audioTrack&lt;/code&gt; already, so the wiring is small; the open question is the UI affordance, same as PiP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caption track UI.&lt;/strong&gt; You can pass &lt;code&gt;&amp;lt;track&amp;gt;&lt;/code&gt; elements but the component doesn't render a captions menu. Native browser controls (&lt;code&gt;controls={true}&lt;/code&gt;) handle this for now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An actual stable Nuxt module ecosystem release&lt;/strong&gt; — currently the module exists and works, but isn't on &lt;a href="https://nuxt.com/modules" rel="noopener noreferrer"&gt;https://nuxt.com/modules&lt;/a&gt;. That's a separate process I'll do when the API has settled for a quarter or two.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# React&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @glitchlab/react-video-player hls.js

&lt;span class="c"&gt;# Vue&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @glitchlab/vue-video-player hls.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// React + Next.js App Router&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReactVideoPlayer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/react-video-player&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/react-video-player/style.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactVideoPlayer&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/videos/hero.m3u8"&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Nuxt 3&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/vue-video-player/nuxt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/vue-video-player/style.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- anywhere in your Nuxt app --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;VueVideoPlayer&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/videos/hero.m3u8"&lt;/span&gt; &lt;span class="na"&gt;:controls=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Live demo: &lt;strong&gt;&lt;a href="https://video-player-playgraound.vercel.app/" rel="noopener noreferrer"&gt;https://video-player-playgraound.vercel.app/&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
React package: &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@glitchlab/react-video-player" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@glitchlab/react-video-player&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Vue package: &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@glitchlab/vue-video-player" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@glitchlab/vue-video-player&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;strong&gt;&lt;a href="https://github.com/im-fahad/react-video-player" rel="noopener noreferrer"&gt;https://github.com/im-fahad/react-video-player&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://github.com/im-fahad/vue-video-player" rel="noopener noreferrer"&gt;https://github.com/im-fahad/vue-video-player&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Issues, PRs, and feature requests welcome. If something's broken in your setup, the playground is the fastest way to reproduce it — drop your URL in, screenshot what you see, file an issue.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you got value from this, the cheapest way to support is hitting the GitHub star button or sharing the playground link with someone fighting the same CSS-bleed problem. Both packages are MIT and will stay that way.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>vue</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Implementing User Authentication in Next.js 14 using AuthJs Credentials (REST API)</title>
      <dc:creator>Abdullah Al Fahad</dc:creator>
      <pubDate>Mon, 19 Feb 2024 04:55:13 +0000</pubDate>
      <link>https://dev.to/imfahad/implementing-user-authentication-in-nextjs-14-using-authjs-credentials-rest-api-20jd</link>
      <guid>https://dev.to/imfahad/implementing-user-authentication-in-nextjs-14-using-authjs-credentials-rest-api-20jd</guid>
      <description>&lt;p&gt;Let’s talk step by step about how to implement Next.js 14’s user authentication with Auth.js (credentials) and REST API.&lt;/p&gt;

&lt;p&gt;To start, begin by creating a &lt;a href="https://nextjs.org/docs/app/api-reference/create-next-app"&gt;Next.js&lt;/a&gt; application.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx create-next-app@latest&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then, integrate the &lt;a href="https://authjs.dev/reference/nextjs"&gt;Auth.js&lt;/a&gt; library into your application.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install next-auth@beta&lt;/code&gt;&lt;br&gt;
Next, you will need to add a &lt;code&gt;AUTH_SECRET&lt;/code&gt; and &lt;code&gt;NEXTAUTH_URL&lt;/code&gt; to your &lt;code&gt;env&lt;/code&gt; file. Although during development you could leave &lt;code&gt;AUTH_SECRET&lt;/code&gt; it empty, during production it would raise an error.&lt;/p&gt;

&lt;p&gt;Now, proceed to create the &lt;code&gt;auth.ts&lt;/code&gt; file within your &lt;code&gt;/src&lt;/code&gt; directory. This file will handle authentication by interfacing with a REST API, specifically for login functionality. Depending on your API structure, the response may vary, but you can reference the example API response provided here to guide your implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import type {NextAuthConfig} from "next-auth";
import NextAuth from "next-auth";

import CredentialsProvider from "next-auth/providers/credentials";
import $axios from "@/lib/Axios";

async function login(credentials: any) {
    try {
        return await $axios.post("your-login-api", credentials).then((res: any) =&amp;gt; {
            const {user} = res;
            return {
                name: user.name,
                email: user.email,
                image: user.profile_photo,
                accessToken: res.access_token,
                // If you need any other information you can add here...
            };
        });
    } catch (e) {
        throw new Error("Something went wrong.");
    }
}

export const config: NextAuthConfig = {
    pages: {signIn: "/login"},
    providers: [
        CredentialsProvider({
            name: "Credentials",
            credentials: {
                email: {label: "Email", type: "email", placeholder: "example@email.com"},
                password: {label: "Password", type: "password", placeholder: "******"}
            },
            async authorize(credentials,) {
                try {
                    return login(credentials);
                } catch (e) {
                    return {};
                }
            },
        }),
    ],
    callbacks: {
        async jwt({user, token}) {
            if (user) {
                token.user = user;
            }
            return token;
        },
        async session({session, token}: any) {
            session.user = token.user;
            return session;
        },
    },
    debug: process.env.NODE_ENV === "development",
};

export const {handlers, auth, signIn, signOut} = NextAuth(config);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;jwt()&lt;/code&gt; callback is called user when the user first logs in. The user object will be populated with the object that is returned from the authorize function. The object that is returned from the &lt;code&gt;jwt&lt;/code&gt; callback is what will be saved on the session cookie.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;session()&lt;/code&gt; callback receives the session cookie content in its token parameter. Whatever is returned from this callback is what will be presented when &lt;code&gt;useSession&lt;/code&gt; or &lt;code&gt;getServerSession&lt;/code&gt; is called.&lt;/p&gt;

&lt;p&gt;Next, establish a &lt;code&gt;api&lt;/code&gt; directory within either your &lt;code&gt;/src/app&lt;/code&gt; or &lt;code&gt;/app&lt;/code&gt; directory. Inside this api directory, create a &lt;code&gt;route.ts&lt;/code&gt; file within the &lt;code&gt;api/auth/[...nextauth]&lt;/code&gt; directory structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {handlers} from "@/auth";

export const {GET, POST} = handlers;
Then create another route.ts file inside api/protected directory.

import {auth} from "@/auth";

export const GET = auth((req) =&amp;gt; {
    if (req.auth) {
        return Response.json({data: "Protected data"});
    }

    return Response.json({message: "Not authenticated"}, {status: 401});
}) as any;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your &lt;code&gt;layout.tsx&lt;/code&gt; file, you'll utilize the &lt;code&gt;SessionProvider&lt;/code&gt; to manage session data. This component enables your application to interact with the user authentication state across various components. You can use it to display different content or UI elements based on whether a user is authenticated or not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";
import React from "react";
import {SessionProvider} from "next-auth/react";
...

export default function RootLayout({children}: { children: React.ReactNode }) {
    return (
        &amp;lt;SessionProvider&amp;gt;
            &amp;lt;html lang="en"&amp;gt;
            &amp;lt;body className="pintoe"&amp;gt;
                {children}
            &amp;lt;/body&amp;gt;
            &amp;lt;/html&amp;gt;
        &amp;lt;/SessionProvider&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well done, your &lt;a href="https://authjs.dev/reference/nextjs"&gt;Auth.js&lt;/a&gt; setup is complete. Now you can use your session data on every page (route). ex: &lt;code&gt;app/pages.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";

import {useSession} from "next-auth/react";
import Image from "next/image";

export default function HomePage() {
    const {data: session} = useSession();

    return (
        &amp;lt;div&amp;gt;
            &amp;lt;h2&amp;gt;{session?.user.name}&amp;lt;/h2&amp;gt;

            &amp;lt;Image src={session?.user.image} alt="User Avatar" width={200} height={200}/&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can use a higher order component to protect authenticated routes. On my side, I create a &lt;code&gt;AuthGuard.tsx&lt;/code&gt; file in &lt;code&gt;src/&lt;/code&gt; components directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";
import React, {useEffect, useState} from "react";
import {useSession} from "next-auth/react";
import {usePathname, useRouter} from "next/navigation";

export default function AuthGuard({children}: { children: React.ReactNode }) {
    const {data: status}: any = useSession();
    const router = useRouter();
    const pathname = usePathname();
    const [loading, setLoading] = useState&amp;lt;boolean&amp;gt;(true);

    const guestRoutes: string[] = [
        "/login",
        "/register"
    ];

    useEffect(() =&amp;gt; {
        const isGuestRoute = !!guestRoutes.find(route =&amp;gt; route === pathname);
        setLoading(true);

        if (status === "loading") {
            return;
        } else if (status === "authenticated" &amp;amp;&amp;amp; isGuestRoute) {
            router.push("/");
        } else if (status === "unauthenticated" &amp;amp;&amp;amp; !isGuestRoute) {
            router.push("/login");
        } else {
            setLoading(false);
        }
    }, [pathname, status]);

    return (loading ?
        &amp;lt;div&amp;gt;
            Loading...
        &amp;lt;/div&amp;gt; : children);
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the &lt;code&gt;AuthGuard&lt;/code&gt; component in your &lt;code&gt;layout.tsx&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";
import "@/assets/styles/tailwind.css";
import React from "react";
import {SessionProvider} from "next-auth/react";
import AuthGuard from "@/lib/AuthGurad";

export default function RootLayout({children}: { children: React.ReactNode }) {
    return (
        &amp;lt;SessionProvider&amp;gt;
            &amp;lt;html lang="en"&amp;gt;
            &amp;lt;body className="pintoe"&amp;gt;
            &amp;lt;AuthGuard&amp;gt;
                {children}
            &amp;lt;/AuthGuard&amp;gt;
            &amp;lt;/body&amp;gt;
            &amp;lt;/html&amp;gt;
        &amp;lt;/SessionProvider&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the AuthGuard component will automatically handle guest/auth route redirects based on login session data.&lt;/p&gt;

&lt;p&gt;Next, we will discuss step by step how to implement &lt;code&gt;redux-toolkit&lt;/code&gt; in &lt;a href="https://nextjs.org/"&gt;Next.js 14&lt;/a&gt; and how to configure &lt;a href="https://axios-http.com/"&gt;Axios&lt;/a&gt; to get &lt;code&gt;access-token&lt;/code&gt; from the auth session.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
