pnpm/pnpm · error · PnpmError

SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER

SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER

Error message

${specifier} isn't supported by any available resolver.

What it means

The default resolver dispatches a wanted dependency through a fixed chain — custom resolvers, npm, jsr:, git, tarball URLs, the local schemes (file:/link:/workspace:), node:/deno:/bun: runtime specifiers, named registries, and finally bare local paths. If every resolver declines, resolve throws SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER naming the alias and bare specifier.

Source

Thrown at pnpm11/resolving/default-resolver/src/index.ts:158

          await _resolveFromLocalScheme(wantedDependency as { bareSpecifier: string }, opts)
        )) ??
        await _resolveNodeRuntime(wantedDependency, opts) ??
        await _resolveDenoRuntime(wantedDependency, opts) ??
        await _resolveBunRuntime(wantedDependency, opts) ??
        // Named-registry runs between the explicit local schemes above and the
        // path-shape match below, so `<alias>:@scope/pkg` reaches the configured
        // registry while a colliding `file:`/`link:`/`workspace:` alias cannot
        // hijack the built-in protocols.
        await resolveFromNamedRegistry(wantedDependency, opts as ResolveFromNpmOptions) ??
        (wantedDependency.bareSpecifier
          ? await _resolveFromLocalPath(wantedDependency as { bareSpecifier: string }, opts)
          : null)
      if (!resolution) {
        let specifier = `${wantedDependency.alias ? wantedDependency.alias + '@' : ''}${wantedDependency.bareSpecifier ?? ''}`
        if (specifier !== '') {
          specifier = `"${specifier}"`
        }
        throw new PnpmError(
          'SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER',
          `${specifier} isn't supported by any available resolver.`)
      }
      return resolution
    },
    resolveLatest: async (query, opts) => {
      const info = (await resolveLatestFromNpm(query, opts)) ??
        (await resolveLatestFromJsr(query, opts)) ??
        (await resolveLatestFromGit(query)) ??
        (await resolveLatestFromTarball(query)) ??
        (await resolveLatestFromLocal(query)) ??
        (await _resolveLatestNodeRuntime(query, opts)) ??
        (await _resolveLatestDenoRuntime(query, opts)) ??
        (await _resolveLatestBunRuntime(query, opts)) ??
        (await resolveLatestFromNamedRegistry(query, opts))
      return info
    },
    clearCache,

View on GitHub (pinned to 6261b7f388)

Solutions

  1. Fix the specifier spelling — most commonly workspace:*, file:./…, or a valid registry name/URL
  2. Use a supported form: npm name/range/tag, jsr:@scope/name, git URL (optionally #semver:^1), tarball URL, file:, link:, workspace:, node:/deno:/bun:, or an alias to a named registry
  3. For proprietary protocols, register a customResolver plugin so the chain has a handler

Example fix

// package.json — before
"@scope/tools": "worskpace:*"

// after
"@scope/tools": "workspace:*"
Defensive patterns

Strategy: type-guard

Validate before calling

import fs from 'node:fs'

// Reject specs no built-in resolver can claim before running install.
export function checkSpecifierSupported (alias: string | undefined, spec: string): void {
  const protocols = ['workspace:', 'file:', 'link:', 'jsr:', 'http://', 'https://', 'node:', 'deno:', 'bun:']
  const bare = spec
  const looksLikeProtocol = /^[a-zA-Z][\w+.-]*:/.test(bare)
  if (looksLikeProtocol && !protocols.some((p) => bare.startsWith(p))) {
    throw new Error(`Unsupported protocol in ${alias ?? ''}${alias ? '@' : ''}${spec}`)
  }
  if (!looksLikeProtocol && bare.startsWith('.') && !fs.existsSync(bare)) {
    throw new Error(`Local path dependency does not exist: ${bare}`)
  }
}

Type guard

const SUPPORTED_PREFIXES = ['workspace:', 'file:', 'link:', 'jsr:', 'node:', 'deno:', 'bun:']
const GIT_HOSTS = /^(git|git\+ssh|git\+https|ssh|https?):\/\//
const TARBALL_URL = /^https?:\/\/.+\.(tgz|tar\.gz)(#.*)?$/

export function isResolvableSpecifier (spec: string): boolean {
  if (SUPPORTED_PREFIXES.some((p) => spec.startsWith(p))) return true
  if (GIT_HOSTS.test(spec) || TARBALL_URL.test(spec)) return true
  if (/^\.\.(\/|$)/.test(spec)) return true // bare local path
  return /^[^:/]/.test(spec) // plain npm name/range/tag
}

Try / catch

try {
  await resolveDependencies(deps, opts)
} catch (err) {
  if (err instanceof PnpmError && err.code === 'SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER') {
    // The message quotes the offending alias/spec; fix the manifest or register a custom resolver
    throw new Error(`Unresolvable dependency spec: ${err.message}`)
  }
  throw err
}

Prevention

When it happens

Trigger: A dependency spec no resolver claims: a typo'd protocol ('worskpace:*', 'htp://…'), a protocol pnpm does not support, an alias whose bare part matches nothing, or a bare path that does not exist on disk so the final path resolver declines.

Common situations: Copy-paste typos in package.json specs; protocols from other ecosystems; a proprietary protocol that requires a customResolver plugin that is not registered; deleted local directories referenced by path.

Related errors


AI-assisted analysis of pnpm/pnpm@6261b7f388 (2026-08-17). Data as JSON: /api/errors/8050bae199d37584. Report an issue: GitHub.