vitejs/vite · error · Error

Could not resolve " " imported by " ". Is it installed?

Error message

Could not resolve "${peerDep}" imported by "${parentDep}". Is it installed?

What it means

Injected at runtime by `vite:dep-optimizer`'s rolldown plugin into the pre-bundled module for an optional peer dependency that could not be resolved. When a dep declares an optional peer dep that is not installed, Vite externalizes it under a special namespace and emits code that throws this message in the browser when the module is actually imported. It is a lazy error: optimization succeeds, the failure surfaces only if the app imports that code path.

Solutions

  1. Install the missing peer: `npm install <peerDep>` (the message names both peerDep and parentDep).
  2. If the import is dead code, configure tree-shaking or dynamic-import it so the browser never loads it.
  3. Mark the peer as external (`build.rollupOptions.external`) if it is provided by the host environment at runtime.
  4. Update the parent dependency; newer versions may handle the optional peer more gracefully.
  5. If using a monorepo, ensure the peer is hoisted to where the parent dep resolves it.

Example fix

// before: @some-lib optionally peers on 'chart.js' which is uninstalled
import Chart from '@some-lib/chart' // throws at runtime

// after
// $ npm install chart.js
import Chart from '@some-lib/chart'
Defensive patterns

Strategy: try-catch

Validate before calling

import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)

function assertPeerInstalled(parentDep: string, peerDep: string) {
  try {
    const parentPkg = require.resolve(`${parentDep}/package.json`)
    require.resolve(peerDep, { paths: [require('node:path').dirname(parentPkg)] })
  } catch {
    console.warn(`Optional peer '${peerDep}' of '${parentDep}' is missing; runtime import will throw.`)
  }
}

Try / catch

// in app code that may hit the externalized optional peer
try {
  const mod = await import('@some-lib/optional-feature')
} catch (e) {
  if (e instanceof Error && /Could not resolve ".*" imported by ".*"\. Is it installed\?/.test(e.message)) {
    console.warn('Optional peer missing; falling back to default behavior.')
    return null
  }
  throw e
}

Prevention

When it happens

Trigger: rolldownDepPlugin.ts:310 builds `module.exports = {}; throw new Error(...)` for ids under `optionalPeerDepNamespace` (format `...:peerDep:parentDep`). Triggered in the browser when app code imports a module that transitively imports an uninstalled optional peer dependency.

Common situations: A UI library (parent dep) optionally relies on a peer like `react`, `vue`, `@reduxjs/toolkit`, or a charting engine that is not installed. The package marks it `peerDependenciesMeta: { optional: true }`, so installers skip it, but the bundler still sees the import. Version mismatch where the peer is installed under a different name.

Related errors


AI-assisted analysis of vitejs/vite@b4d66fee14 (2026-08-11). Data as JSON: /api/errors/f4bbc469a85cc0a9. Report an issue: GitHub.

Appendix: source

Thrown at packages/vite/src/node/optimizer/rolldownDepPlugin.ts:310

            key !== '__proto__' &&
            key !== 'constructor' &&
            key !== 'splice'
        ) {
            console.warn(\`Module "${path}" has been externalized for browser compatibility. Cannot access "${path}.\${key}" in client code. See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\`)
        }
        }
    }))`,
              }
            }
          }

          if (id.startsWith(optionalPeerDepNamespace)) {
            const path = id.slice(optionalPeerDepNamespace.length)
            const [, peerDep, parentDep] = path.split(':')
            return {
              code:
                'module.exports = {};' +
                `throw new Error(\`Could not resolve "${peerDep}" imported by "${parentDep}". Is it installed?\`)`,
            }
          }
        },
      },
      transform: {
        filter: {
          code: assetImportMetaUrlRE,
        },
        handler(code, id) {
          let s: MagicString | undefined
          const re = new RegExp(assetImportMetaUrlRE)
          const cleanString = stripLiteral(code)

          let match: RegExpExecArray | null
          while ((match = re.exec(cleanString))) {
            const [[startIndex, endIndex], [urlStart, urlEnd]] =
              match.indices as Array<[number, number]>
            if (hasViteIgnoreRE.test(code.slice(startIndex, urlStart))) continue

View on GitHub (pinned to b4d66fee14)