#15016·pnpm

A custom resolver that returns a `file:` or git-hosted tarball without a manifest installs the package without its dependencies

Author: zkochanCreated Sep 17, 2026Updated Sep 17, 2026
Labelstype: bugarea: resolution

pnpm version

12.4.2

Code to reproduce the issue

Follow-up to #15000, which reported the same defect for a remote https: tarball and is fixed by #15013.

manifest is optional in the ResolveResult a pnpmfile resolvers hook returns, and the hook's resolution is unconstrained. A resolver can therefore claim a dependency and answer with a file: tarball, or with a git host's archive URL:

javascript
module.exports = {
  resolvers: [
    {
      canResolve: (wanted) => wanted.alias === 'debug',
      async resolve () {
        return {
          id: '[email protected]',
          resolution: { tarball: 'file:./vendor/debug-4.3.4.tgz', integrity: 'sha512-...' },
        }
      },
    },
  ],
}

Expected behavior

The package installs with its own dependencies, as it does for an https: tarball after #15013.

Actual behavior

MissingTarballMetadata::of in pnpm/crates/package-manager/src/prefetching_resolver.rs excludes file: and git-hosted tarball URLs from the resolve-time archive read. The exclusion is right for the resolvers that own those shapes: resolving-local-resolver reads a file: tarball's manifest itself, and resolving-git-resolver reads the archive's manifest and hashes it. A custom resolver bypasses both, so its result reaches walk::fallback_manifest, which synthesizes an identity-only manifest, and the package installs with no dependencies.

Additional information

The two shapes need different work and neither is a variation of the other:

  • file:fetch_and_extract_once already dispatches a file: URL to the local reader, so FetchTarballForResolution works on one as it stands. What is missing is the relative-path rewrite: the install pass resolves a relative file: against the workspace root in local_file_tarball_install_url, and the resolve-time read would have to derive the same absolute URL.
  • git-hosted — the read needs manifest_subdir set from the resolution's path, as resolving-git-resolver does for #path:/packages/foo. It must also leave the integrity alone: a git-hosted archive is anchored by its commit SHA, and the exclusion by URL rather than by the git_hosted flag is deliberate, because that flag is tamper-prone lockfile input. Note also that such an archive is not the package until the fetcher has run prepare and the packlist over it, so its raw root manifest and the installed one can differ.

Raised in review of #15013 by both CodeRabbit-adjacent reviewers on that PR; filed separately rather than carried there because neither shape was reported and both change behavior the local and git resolvers currently own.

Node.js version

24.18.1

Operating System

macOS


Written by an agent (Claude Code, claude-opus-5).