Initializing Enclave...

Fixing 'Module not found: Can't resolve react/jsx-runtime' in Webpack 5 Component Libraries with Next.js 12

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–20 mins


TL;DR

  • What broke: Your component library's Webpack 5 bundle is shipping its own copy of react/jsx-runtime instead of externalizing it, causing a module resolution collision or a missing-module hard crash in the consuming Next.js 12 app.
  • How to fix it: Explicitly add react/jsx-runtime (alongside react and react-dom) to the externals array in your library's webpack.config.js, and declare it in peerDependencies in package.json.
  • Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your webpack.config.js and package.json and get corrected output without sending your config to a third-party server.

The Incident (What does the error mean?)

Raw error output from next build or webpack CLI:

Module not found: Can't resolve 'react/jsx-runtime'
  > in ./node_modules/your-component-lib/dist/index.js

error - ./node_modules/your-component-lib/dist/index.js
Module not found: Can't resolve 'react/jsx-runtime'

Immediate consequence: The Next.js 12 build halts entirely. No static pages are generated. Production deployment is blocked. This is not a warning — it is a hard fatal error.

The underlying mechanism: React 17+ introduced the new JSX transform, which auto-imports from react/jsx-runtime instead of requiring React in scope. When your library is compiled with the new JSX transform enabled (default in most modern Babel/TSC configs) but react/jsx-runtime is not externalized, Webpack 5 bundles a reference to that module path into your dist/ output. The consuming app then tries to resolve react/jsx-runtime from inside node_modules/your-component-lib/dist/, where it does not exist.


The Attack Vector / Blast Radius

This is a dependency isolation failure, not a security CVE — but the blast radius in a monorepo or shared design system is severe:

  1. Every downstream app breaks simultaneously. If your component library is published to a private registry and consumed by 5 Next.js apps, a single bad publish takes down all 5 builds.
  2. Duplicate React instances. Even if the consuming app has React installed, if react/jsx-runtime is bundled inside your library dist instead of externalized, you risk two separate React runtime instances in the same browser context. This causes the Invalid hook call error and breaks all hooks silently at runtime — harder to debug than the build error.
  3. Next.js 12 + Webpack 5 module resolution is stricter. Webpack 5 removed automatic polyfilling of Node.js core modules and tightened exports field resolution. react/jsx-runtime relies on the exports field in React's package.json. If your library bundles it, the path resolution inside the dist bundle does not have the correct node_modules context to walk up to.
  4. CI/CD pipelines fail silently on cache hits. If your pipeline caches node_modules and the bad library version is cached, the error reappears after cache invalidation, making it look like an infrastructure flake rather than a code defect.

How to Fix It (The Solution)

Basic Fix — Externalize react/jsx-runtime in your library's Webpack config

// webpack.config.js (library bundle)
  externals: {
-   react: 'react',
-   'react-dom': 'react-dom',
+   react: 'react',
+   'react-dom': 'react-dom',
+   'react/jsx-runtime': 'react/jsx-runtime',
+   'react/jsx-dev-runtime': 'react/jsx-dev-runtime',
  },

Why both jsx-runtime and jsx-dev-runtime? Babel and TSC emit jsx-dev-runtime imports in development mode builds. Externalizing only jsx-runtime leaves dev builds broken.

Fix package.json — Declare correct peerDependencies

// package.json (your component library)
  "peerDependencies": {
-   "react": ">=16.8.0",
-   "react-dom": ">=16.8.0"
+   "react": ">=17.0.0",
+   "react-dom": ">=17.0.0"
  },
+ "peerDependenciesMeta": {
+   "react": { "optional": false },
+   "react-dom": { "optional": false }
+ }

Bumping the peer dep floor to >=17.0.0 is intentional — react/jsx-runtime does not exist in React 16. If you must support React 16, you need a Babel config that uses the classic JSX runtime (@babel/plugin-transform-react-jsx with runtime: 'classic').

Enterprise Best Practice — Use externalsType + regex for future-proofing

// webpack.config.js
  module.exports = {
    mode: 'production',
    output: {
      library: { type: 'commonjs2' },
    },
-   externals: {
-     react: 'react',
-     'react-dom': 'react-dom',
-   },
+   externalsType: 'commonjs',
+   externals: [
+     // Externalize all react/* subpath exports
+     /^react(\/.*)?$/,
+     /^react-dom(\/.*)?$/,
+   ],
  };

Using a regex pattern (/^react(\/.*)$/) future-proofs against any new React subpath exports (e.g., react/compiler-runtime in React 19+) without requiring manual config updates per release.

Bonus — Verify with webpack-bundle-analyzer

After rebuilding, confirm react and react/jsx-runtime are not present in your dist bundle:

npx webpack-bundle-analyzer dist/stats.json
# react/* should appear as external, not as a bundled module chunk

💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

1. Add a bundle validation step post-build

# In your library's CI pipeline (GitHub Actions / GitLab CI)
- name: Assert react is externalized
  run: |
    if grep -r '"react/jsx-runtime"' dist/; then
      echo "FATAL: react/jsx-runtime is bundled. Externalization failed."
      exit 1
    fi

2. Use publint to catch peer dependency issues before publish

npx publint
# Catches: missing peerDependencies, incorrect exports fields, CJS/ESM mismatches

Add to your prepublishOnly script:

// package.json
  "scripts": {
+   "prepublishOnly": "publint && npm run build"
  }

3. Enforce with are-the-types-wrong + attw

npx @arethetypeswrong/cli ./dist/your-lib.tgz
# Validates that module resolution behaves correctly for CJS and ESM consumers

4. Lock peer dep validation in PR checks

If using Nx or Turborepo, add a lint rule to your project.json or turbo.json that runs publint on every affected library package before the build task executes. This catches the misconfiguration at PR time, not at publish time.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →