Skip to content

fix(site): install musl resvg binary so Netlify can bundle the OG function#1759

Merged
decepulis merged 1 commit into
mainfrom
claude/site-build-failure-main-cob1av
Jun 30, 2026
Merged

fix(site): install musl resvg binary so Netlify can bundle the OG function#1759
decepulis merged 1 commit into
mainfrom
claude/site-build-failure-main-cob1av

Conversation

@decepulis

@decepulis decepulis commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

The site build has been failing on main since the Astro 7 / Vite 8 upgrade because we pulled in a stricter version of the Netlify adapter that can't cope with how pnpm installs native addons.

This adds one pnpm config block to fix it.

The deets

Note

I edited this, but, there's still some AI jargon in there my brain failed to totally parse. Sorry. Here be vibe-dragons.

When Astro deploys an SSR route to Netlify, the @astrojs/netlify adapter has to package that route as a Netlify Function. To do that, it runs @vercel/nft (Node File Trace) over the server entry to discover every file the function will need at runtime, then copies those files into the function bundle.

Our OG-image route is server-rendered and uses @resvg/resvg-js to rasterize SVG → PNG. resvg is a native Rust addon, so it doesn't ship as one package — it ships as a family of platform-specific binaries declared as optionalDependencies, and the loader picks the right .node at runtime.

Two reasonable behaviors collide here:

  1. nft over-traces native addons. It statically sees resvg require-ing several platform binaries. It can prune by OS (the build runs on Linux, so darwin/win32 drop out), but it can't tell glibc from musl statically — so it keeps both Linux variants (gnu and musl) in the file list.
  2. pnpm only installs the binary that matches the host. Netlify is glibc, so pnpm installs …-linux-x64-gnu and skips …-linux-x64-musl (the package declares libc: [musl], which pnpm filters out). The musl path never exists on disk.
  3. The adapter's copy step calls fs.realpath on every traced file — including the musl binary nft listed but pnpm never installed. realpath on a missing path throws ENOENT, the astro:build:done hook crashes unhandled, and the whole build exits non-zero:
[ERROR] [@astrojs/netlify] An unhandled error occurred while running the "astro:build:done" hook
ENOENT: no such file or directory, realpath
  '/opt/build/repo/node_modules/.pnpm/@resvg+resvg-js@2.6.2/node_modules/@resvg/resvg-js-linux-x64-musl'
    at async copyDependenciesToFunction (@astrojs/netlify/dist/lib/nft.js:40:26)

The fix

Make the missing binary present so nft's realpath resolves. pnpm has a knob for exactly this — supportedArchitectures — which installs optional deps for architectures beyond the host's:

# pnpm-workspace.yaml
supportedArchitectures:
  os: [current]
  cpu: [current]
  libc: [current, musl]

pnpm now installs both Linux variants, the musl symlink exists, the copy step succeeds, and the unused musl .node rides along in the bundle (~1 MB; the glibc binary still loads at runtime). os: [current] keeps the OS filter intact, so it only adds musl-libc Linux binaries — macOS/Windows dev installs are unaffected.

Alternative considered: the adapter's excludeFiles option could drop the musl path from the trace instead, avoiding the dead binary. But it's an exact-string match against a hashed .pnpm/… path, so it's brittle across version bumps. supportedArchitectures is the more durable fix; the cost is ~1 MB of unused binary in the function.


https://claude.ai/code/session_017FVfYwxbfNWwUkzhRdTr3k


Note

Low Risk
Workspace-only install policy change for optional native binaries; no application logic or auth/data paths touched.

Overview
Adds supportedArchitectures to pnpm-workspace.yaml so pnpm installs both glibc and musl Linux optional binaries for native deps like @resvg/resvg-js, not only the host libc.

This unblocks Netlify site builds that started failing after the Astro 7 / @astrojs/netlify v8 upgrade: @vercel/nft traces both Linux resvg variants, but the adapter’s copy step calls realpath on every traced path—when the musl package was never installed, the build crashed with ENOENT. Installing the musl variant gives nft a real path to resolve; runtime still uses the glibc binary on Netlify (~1 MB extra in the function bundle).

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

…ction

The site build fails on Netlify during the @astrojs/netlify (v8) adapter's
`astro:build:done` hook with:

  ENOENT: no such file or directory, realpath
  '.../@resvg+resvg-js@2.6.2/node_modules/@resvg/resvg-js-linux-x64-musl'

The adapter bundles the serverless function by tracing the server entry with
@vercel/nft, then calling fs.realpath on every traced file. The OG image route
(/og/[...path].png) is server-rendered and pulls in the native @resvg/resvg-js
binding. nft can't distinguish glibc from musl statically, so it traces both
Linux binaries, but pnpm only installs the glibc variant on Netlify — realpath
on the missing musl binary throws and the whole build exits non-zero.

This is not a regression in any feature commit. The breakage was introduced by
the Astro 7 / Vite 8 upgrade (which bumped @astrojs/netlify 7 -> 8); the older
v7 adapter bundled resvg fine. It only surfaces on builds that actually run:
netlify.toml gates the site build on `turbo query affected --packages site`, so
commits that don't touch site/ skip the build entirely and appear "green."

Add `supportedArchitectures` so pnpm also installs the musl Linux binaries,
giving nft a real file to resolve. Verified that this materializes the
resvg-js-linux-x64-musl symlink and leaves pnpm-lock.yaml unchanged (so
--frozen-lockfile installs stay valid).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017FVfYwxbfNWwUkzhRdTr3k
@netlify

netlify Bot commented Jun 30, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit d67c1e6
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a44263b7814b30008f5370e
😎 Deploy Preview https://deploy-preview-1759--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

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

@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Jun 30, 2026 8:25pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Initial
/video (default) 45.70 kB
/video (default + hls) 185.25 kB
/video (minimal) 45.70 kB
/video (minimal + hls) 185.38 kB
/audio (default) 39.03 kB
/audio (minimal) 38.13 kB
/background 4.20 kB
Media (10)
Entry Initial
/media/background-video 1.14 kB
/media/container 1.72 kB
/media/dash-video 214.72 kB
/media/hlsjs-video 141.49 kB
/media/mux-audio 163.89 kB
/media/mux-video 163.96 kB
/media/native-hls-video 9.05 kB
/media/simple-hls-audio-only 17.15 kB
/media/simple-hls-video 18.72 kB
/media/vimeo-video 12.31 kB
Players (5)
Entry Initial
/video/player 8.22 kB
/audio/player 5.38 kB
/background/player 3.92 kB
/live-video/player 7.64 kB
/live-audio/player 5.39 kB
Skins (30)
Entry Type Initial
/video/minimal-skin.css css 5.71 kB
/video/skin.css css 5.68 kB
/video/minimal-skin js 45.67 kB
/video/minimal-skin.tailwind js 46.35 kB
/video/skin js 45.69 kB
/video/skin.tailwind js 46.49 kB
/audio/minimal-skin.css css 3.85 kB
/audio/skin.css css 3.76 kB
/audio/minimal-skin js 38.15 kB
/audio/minimal-skin.tailwind js 38.59 kB
/audio/skin js 39.03 kB
/audio/skin.tailwind js 39.55 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.71 kB
/live-video/skin.css css 5.68 kB
/live-video/minimal-skin js 44.65 kB
/live-video/minimal-skin.tailwind js 45.21 kB
/live-video/skin js 44.30 kB
/live-video/skin.tailwind js 44.89 kB
/live-audio/minimal-skin.css css 3.85 kB
/live-audio/skin.css css 3.76 kB
/live-audio/minimal-skin js 30.77 kB
/live-audio/minimal-skin.tailwind js 30.13 kB
/live-audio/skin js 31.79 kB
/live-audio/skin.tailwind js 31.29 kB
/global.css css 176 B
/shared.css css 104 B
/tailwind.css css 228 B
/skin-element js 1.45 kB
UI Components (39)
Entry Initial
/ui/airplay-button 2.36 kB
/ui/alert-dialog 2.45 kB
/ui/alert-dialog-close 2.22 kB
/ui/alert-dialog-description 2.17 kB
/ui/alert-dialog-title 2.13 kB
/ui/audio-track-radio-group 2.87 kB
/ui/buffering-indicator 2.27 kB
/ui/captions-button 2.41 kB
/ui/captions-radio-group 2.76 kB
/ui/cast-button 2.31 kB
/ui/compounds 2.96 kB
/ui/controls 2.70 kB
/ui/error-dialog 2.73 kB
/ui/fullscreen-button 2.32 kB
/ui/hotkey 2.35 kB
/ui/menu 2.75 kB
/ui/mute-button 2.34 kB
/ui/pip-button 2.35 kB
/ui/play-button 2.36 kB
/ui/playback-rate-button 2.33 kB
/ui/playback-rate-radio-group 2.77 kB
/ui/popover 2.68 kB
/ui/poster 2.19 kB
/ui/quality-radio-group 2.77 kB
/ui/seek-button 2.27 kB
/ui/seek-indicator 2.37 kB
/ui/seek-indicator-value 450 B
/ui/slider 2.74 kB
/ui/status-announcer 2.26 kB
/ui/status-indicator 2.27 kB
/ui/status-indicator-value 384 B
/ui/thumbnail 2.16 kB
/ui/time 2.71 kB
/ui/time-slider 2.75 kB
/ui/tooltip 2.67 kB
/ui/volume-indicator 2.32 kB
/ui/volume-indicator-fill 379 B
/ui/volume-indicator-value 399 B
/ui/volume-slider 2.75 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Initial
/video (default) 37.83 kB
/video (default + hls) 176.21 kB
/video (minimal) 38.18 kB
/video (minimal + hls) 176.61 kB
/audio (default) 30.67 kB
/audio (minimal) 30.75 kB
/background 754 B
Media (9)
Entry Initial
/media/background-video 575 B
/media/dash-video 213.16 kB
/media/hlsjs-video 140.07 kB
/media/mux-audio 162.52 kB
/media/mux-video 162.57 kB
/media/native-hls-video 7.41 kB
/media/simple-hls-audio-only 15.56 kB
/media/simple-hls-video 17.14 kB
/media/vimeo-video 10.58 kB
Skins (27)
Entry Type Initial
/tailwind.css css 228 B
/video/minimal-skin.css css 5.62 kB
/video/skin.css css 5.57 kB
/video/minimal-skin js 38.16 kB
/video/minimal-skin.tailwind js 44.19 kB
/video/skin js 37.81 kB
/video/skin.tailwind js 43.76 kB
/audio/minimal-skin.css css 3.72 kB
/audio/skin.css css 3.62 kB
/audio/minimal-skin js 30.65 kB
/audio/minimal-skin.tailwind js 32.75 kB
/audio/skin js 30.60 kB
/audio/skin.tailwind js 34.75 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.62 kB
/live-video/skin.css css 5.57 kB
/live-video/minimal-skin js 33.29 kB
/live-video/minimal-skin.tailwind js 39.23 kB
/live-video/skin js 33.27 kB
/live-video/skin.tailwind js 39.22 kB
/live-audio/minimal-skin.css css 3.72 kB
/live-audio/skin.css css 3.62 kB
/live-audio/minimal-skin js 21.94 kB
/live-audio/minimal-skin.tailwind js 24.94 kB
/live-audio/skin js 21.98 kB
/live-audio/skin.tailwind js 25.09 kB
UI Components (33)
Entry Initial
/ui/airplay-button 2.20 kB
/ui/alert-dialog 2.12 kB
/ui/audio-track 2.10 kB
/ui/buffering-indicator 2.06 kB
/ui/captions-button 2.19 kB
/ui/captions-radio-group 2.10 kB
/ui/cast-button 2.12 kB
/ui/controls 1.97 kB
/ui/error-dialog 2.22 kB
/ui/fullscreen-button 2.12 kB
/ui/gesture 2.19 kB
/ui/hotkey 2.19 kB
/ui/live-button 2.08 kB
/ui/menu 2.20 kB
/ui/mute-button 2.23 kB
/ui/pip-button 2.19 kB
/ui/play-button 2.15 kB
/ui/playback-rate 2.07 kB
/ui/playback-rate-button 2.20 kB
/ui/popover 2.51 kB
/ui/poster 2.01 kB
/ui/quality 2.05 kB
/ui/seek-button 2.20 kB
/ui/seek-indicator 2.06 kB
/ui/slider 2.16 kB
/ui/status-announcer 2.06 kB
/ui/status-indicator 2.07 kB
/ui/thumbnail 2.01 kB
/ui/time 1.94 kB
/ui/time-slider 2.21 kB
/ui/tooltip 2.63 kB
/ui/volume-indicator 2.02 kB
/ui/volume-slider 2.20 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (14)
Entry Initial
. 9.22 kB
/dom 17.44 kB
/dom/media/custom-media-element 2.09 kB
/dom/media/dash 208.96 kB
/dom/media/google-cast 4.04 kB
/dom/media/hls-js 136.00 kB
/dom/media/media-host 1.25 kB
/dom/media/media-played-ranges 576 B
/dom/media/mux 151.26 kB
/dom/media/native-hls 3.07 kB
/dom/media/simple-hls 16.49 kB
/dom/media/simple-hls-audio-only 14.92 kB
/dom/media/vimeo 9.86 kB
/media/predicate 563 B
🏷️ @videojs/element — no changes
Entries (2)
Entry Initial
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Initial
. 1.39 kB
/html 696 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Initial
/array 104 B
/dom 2.26 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 231 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (4)
Entry Initial
. 4.45 kB
/dom 6.33 kB
/hls 15.37 kB
/background-video 12.85 kB

ℹ️ How to interpret

JS sizes are initial static graph totals (minified + brotli). Lazy dynamic chunks are shown separately when present.

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

Run pnpm size locally to check current initial sizes.

@decepulis decepulis marked this pull request as ready for review June 30, 2026 21:31
@decepulis decepulis merged commit 7c3be22 into main Jun 30, 2026
26 checks passed
@decepulis decepulis deleted the claude/site-build-failure-main-cob1av branch June 30, 2026 22:56
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