Back to core workflows Fix dependency resolution Tune package metadata Validate before publishing

Fixing Missing Platform Binaries in optionalDependencies

Tools such as esbuild, Rollup 4, SWC, Lightning CSS, Biome and sharp ship their native code as a family of per-platform packages listed in optionalDependencies. The package manager installs only the one that matches the current operating system, CPU and C library. When that selection goes wrong — usually because a lockfile was generated on a different machine, or optional dependencies were skipped — the tool crashes at startup with a message about a missing module. This guide explains how the selection works, why it breaks, and how to make installs reliable across macOS laptops, Linux CI and Docker images.

Exact symptoms and error messages

Each tool words the failure differently, but all of them are the same bug. Rollup 4:

Error: Cannot find module @rollup/rollup-linux-x64-gnu. npm has a bug related to optional dependencies
(https://github.com/npm/cli/issues/4828). Please try `npm i` again after removing both
package-lock.json and node_modules directory.

esbuild, when the installed binary belongs to another platform:

Error:
You installed esbuild for another platform than the one you're currently using.
This won't work because esbuild is written with native code and needs to
install a platform-specific binary executable.

Specifically the "@esbuild/darwin-arm64" package is present but this platform
needs the "@esbuild/linux-x64" package instead.

SWC and Lightning CSS:

Error: Failed to load native binding
Error: Cannot find module '../lightningcss.linux-x64-musl.node'

These appear at the first command that loads the tool — vite build, next build, a test run — not during install, because optional dependencies are allowed to fail silently.

Root cause analysis

A native-tool package declares every platform build as optional. Each platform package carries os, cpu and, for Linux, libc fields; the package manager skips any whose fields do not match the machine, and does not fail if one cannot be installed. At runtime, a small JavaScript loader works out the current platform and requires the matching package.

How a native tool picks its platform binary The main package lists platform packages as optional; the installer filters them by os, cpu and libc; the runtime loader requires the one matching process.platform and arch. rollup optionalDependencie s lists 20 platform packages installer filter os, cpu, libc vs this machine one package installed @rollup/rollup-linux-x 64-gnu runtime loader require() by process.platform + arch
The installer and the runtime loader must agree on the platform — any mismatch surfaces as a missing module at startup.

Three situations break that agreement:

  1. A lockfile that only records one platform. Certain npm versions, when the lockfile is generated on macOS, write entries only for the optional packages that were actually installed. A later npm ci on Linux follows the lockfile faithfully and never installs the Linux binary. This is the npm bug the Rollup message links to.
  2. node_modules copied between machines. Mounting a host node_modules into a Linux container, or caching node_modules from a macOS runner and restoring it on Linux, gives the runtime a binary built for the wrong platform.
  3. Optional dependencies disabled. npm ci --omit=optional, --no-optional, pnpm's --no-optional, or an .npmrc with optional=false skip every platform package. So does a lockfile generated with those settings.

A fourth, rarer cause is a libc mismatch: Alpine Linux uses musl, and a lockfile or cache built on glibc Debian contains -gnu binaries that fail to load on Alpine.

Resolution and configuration patch

Start by identifying which of the causes applies, then fix the lockfile or the install environment rather than the tool.

Diagnosing a missing platform binary Branches on whether node_modules came from another machine, whether optional deps were omitted, or whether the lockfile lacks other platforms. Binary missing at runtime compare process.platform with what is installed node_modules from another OS install inside the target environment copied tree optional deps skipped remove --omit=optional and optional=false omitted lockfile has one platform regenerate with a current npm, or use pnpm lockfile
Each cause has a different fix, so identify it before deleting anything.

Regenerate the npm lockfile with a current npm. Recent npm 10 and 11 releases record every platform's optional package in package-lock.json. Upgrade npm, then rebuild the lockfile once:

npm install -g npm@latest
rm -rf node_modules package-lock.json
npm install
git add package-lock.json && git commit -m "Regenerate lockfile with all platform binaries"

Check that the lockfile now lists entries such as node_modules/@rollup/rollup-linux-x64-gnu even though you generated it on a Mac.

For pnpm, declare the platforms you support. pnpm records all optional variants in pnpm-lock.yaml, but by default installs only the local one. If you build a node_modules on one machine for use on another — a Docker image built on a Mac, or a deploy bundle — tell pnpm which platforms to install:

# pnpm-workspace.yaml (pnpm 10)
supportedArchitectures:
  os: [current, linux]
  cpu: [current, x64, arm64]
  libc: [current, glibc, musl]

Never ship node_modules across operating systems. In Docker, copy only the manifests and lockfile, then install inside the image:

FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

And add node_modules to .dockerignore so a host tree is never copied in. The same rule applies to CI caches: cache the package manager's download store keyed by OS, not the installed tree.

Re-enable optional dependencies wherever they were turned off. If you used --omit=optional to slim a production image, omit dev dependencies instead (npm ci --omit=dev) — native build tools are usually dev dependencies anyway.

Package manager behaviour for platform-specific optional packages Compares npm, pnpm and Yarn Berry on lockfile coverage, default install and cross-platform install controls. npm 10+ pnpm 9+ Yarn Berry Lockfile lists all platforms yes (older npm: sometimes not) yes yes Installs by default current platform only current platform only current platform only Install for other platforms --os / --cpu flags (npm 10.2+) supportedArchitectures supportedArchitectures Skips optional on request --omit=optional --no-optional via config
Modern versions of all three record every platform in the lockfile; they differ in how you install for a platform other than your own.

How the platform fields are matched

Each platform package in the family is an ordinary npm package with three extra fields that make it installable only on matching machines. Rollup's Linux x64 glibc build, for example, declares:

{
  "name": "@rollup/rollup-linux-x64-gnu",
  "os": ["linux"],
  "cpu": ["x64"],
  "libc": ["glibc"],
  "main": "./rollup.linux-x64-gnu.node"
}

os is compared with process.platform (linux, darwin, win32), cpu with process.arch (x64, arm64, arm, ia32), and libc — honoured by npm 10.4+, pnpm and Yarn Berry — with the C library detected on Linux (glibc or musl). Any mismatch marks the package as unsupported, and because it is listed under optionalDependencies the installer skips it with at most a verbose log line. That silent skip is deliberate: it is what lets one lockfile serve every platform. It is also why the failure appears only at runtime.

The loader side is equally simple. The main package computes a name such as @rollup/rollup-${platform}-${arch}-${libcSuffix} and calls require() on it. If your machine is a platform the tool does not build for at all — FreeBSD, 32-bit ARM, or a very old glibc — no fix in your lockfile will help, and the tool's documentation usually points at a WebAssembly fallback package instead (@rollup/wasm-node, esbuild-wasm), which trades speed for portability.

Worked example: a Mac developer and a Linux CI runner

The most common real-world report comes from a team where developers use Apple Silicon laptops and CI runs on Linux x64. A developer upgrades Vite, which pulls in a new Rollup, and commits the updated lockfile. CI fails immediately with the @rollup/rollup-linux-x64-gnu message.

Inspecting package-lock.json shows an entry for node_modules/@rollup/rollup-darwin-arm64 with full resolution data, and no entries at all for the other nineteen platforms. The developer's npm version predates the fix that records every optional variant. The durable fix is not to add the Linux package by hand but to bring the package-manager version under control: pin npm through the packageManager field, have every developer run through Corepack or a matching version, regenerate the lockfile once, and add a CI step that greps for the Linux entry. After that, lockfiles produced on any developer machine contain every platform, and the failure does not recur.

CLI validation and debug commands

# What does Node think this machine is?
node -p "process.platform + '-' + process.arch"
node -p "process.report.getReport().header.glibcVersionRuntime || 'musl or non-Linux'"

# Which platform packages are installed?
ls node_modules/@rollup/ node_modules/@esbuild/ 2>/dev/null

# Does the lockfile know about the Linux build?
grep -c '"node_modules/@rollup/rollup-linux-x64-gnu"' package-lock.json
grep -n "@rollup/rollup-linux-x64-gnu" pnpm-lock.yaml | head -3

# Load the tool directly to confirm the fix
node -e "require('rollup'); console.log('rollup native binding ok')"
npx esbuild --version

If grep -c prints 0, the npm lockfile is the problem and must be regenerated.

Prevention and CI/CD guardrails

  • Build lockfiles with a current package manager, pinned through the packageManager field so every developer and runner uses the same version — see Pinning the Package Manager with Corepack.
  • Add a lockfile assertion to CI that fails if a required Linux platform package is missing from the lockfile.
  • Cache stores, not trees. Cache ~/.npm or the pnpm store keyed on runner.os and the lockfile hash.
  • Install inside containers. Every Dockerfile runs its own npm ci or pnpm install --frozen-lockfile; .dockerignore excludes node_modules.
  • Match libc. If production runs on Alpine, run at least one CI job on Alpine so musl builds are exercised.

Frequently Asked Questions

Why does deleting node_modules and the lockfile fix it? Because a fresh install with a current npm writes every platform variant into the new lockfile and installs the right binary for the current machine. The steps are sound; the important part is committing the regenerated lockfile so CI gets the same fix.

Can I install the missing platform package manually? Running npm install @rollup/rollup-linux-x64-gnu works as a stopgap, but it adds a platform-specific direct dependency that will break macOS and Windows developers. Fix the lockfile instead.

Why does it only fail on Apple Silicon or ARM CI runners? Those are the platforms most often missing from lockfiles generated on x64 machines, and the platforms most likely to receive a cached tree from a different architecture. Treat arm64 as a first-class platform in your CI matrix.

Related

Dependency Resolution Explained