Node.js — Cannot Find Module for a Platform-Specific Optional Dependency
Fix Cannot find module errors like @swc/core-linux-x64-gnu by understanding optionalDependencies install behavior and locking the platform artifact in CI.
Symptoms
- Error: Cannot find module '@swc/core-linux-x64-gnu' on Linux CI while macOS builds succeed
- The failure occurs inside a native-accelerated toolchain: swc, lightningcss, rolldown, or esbuild shims
- node_modules/@swc/core exists, but the platform binary package beneath it does not
Possible causes
- The platform binary is an optionalDependency that npm skips when the CPU, libc, or OS marker does not match the installing machine
- The lockfile was generated on a different platform and platform-specific optional entries were pruned
- A package manager flag (omit optional or a strict pnpm config) disabled optional dependency installation
- The deployment image installs with a different architecture than the build stage, for example arm64 build and amd64 runtime
Diagnosis steps
Check which platform variants are present
ls node_modules/@swc/ 2>/dev/null; npm ls @swc/core-linux-x64-gnu 2>&1 | head -5Interpretation
A present core package with missing or mismatched platform subpackage confirms the optional artifact was skipped, not that the package is broken.
Verify the marker against the runtime platform
node -p "process.platform + ' ' + process.arch + ' ' + (process.report.getReport().header.glibcVersionRuntime || 'musl')"Interpretation
The triple (platform, arch, libc) must match the variant the toolchain tries to require; glibc versus musl mismatches are the classic Alpine case.
Inspect install flags and lockfile entries
grep -n "omit" .npmrc package.json; grep -n "@swc/core-linux" package-lock.json | head -5Interpretation
omit=optional in config or a lockfile missing the linux variant explains why the binary never installs.
Root cause
Native-accelerated packages declare one platform binary per target as optionalDependencies so a single package.json can serve every OS. The required() module exists only when the matching platform artifact was installed, and the installing environment skipped it because of a marker, lockfile, or architecture mismatch.
Fix
Install on the target platform with the same package manager and lockfile, keep build and runtime stages on the same architecture, remove omit-optional flags, and for Alpine-based images use the glibc-compatible variant or the musl build explicitly.
Risk note
Pinning the binary as a hard dependency makes every other platform install fail. Keep the optional pattern and fix the environment instead.
Verify the fix
Run npm ci inside the exact runtime image and require the native entry directly: node -e "require('@swc/core-linux-x64-gnu')" must exit zero before the toolchain is trusted.
Symptom walkthrough
The module name itself contains the platform triple, which makes the diagnosis faster than most:
Error: Cannot find module '@swc/core-linux-x64-gnu'
Require stack:
- /app/node_modules/@swc/core/index.js
The require stack shows the wrapper asked for the platform artifact. Inspect what actually landed:
ls node_modules/@swc/
# core <- wrapper, present
# core-darwin-arm64? no, this is linux CI
# (missing the linux-x64-gnu artifact)
The wrapper is present but the artifact it requires is not, which is an install-time decision, not a code bug.
Fix in practice
Align the install environment with the runtime environment. In a Dockerfile, install inside the runtime stage or copy node_modules built on the same triple:
FROM node:22-bookworm-slim AS deps
RUN npm ci # lockfile includes linux-x64-gnu variant
FROM node:22-bookworm-slim AS runtime
COPY --from=deps /app/node_modules ./node_modules
Clear the common blockers: remove omit=optional from .npmrc and environment, regenerate the lockfile on Linux if it lost platform entries, and never build on arm64 for an amd64 runtime. On Alpine, the libc suffix matters — either use a glibc-based image or ensure the -musl variant installs.
Verification
In the exact runtime image, run the most direct possible assertion: node -e “require(‘@swc/core-linux-x64-gnu’)” exiting zero proves the artifact resolved. Then run one real build through the toolchain. Add this require probe to CI so a base image change (for example slim switching libc) fails loudly at install time rather than at first compile.