Could pnpm2nix be derivation-per-package?

I’ve been using the most up-to-date version of pnpm2nix which is GitHub - FliegendeWurst/pnpm2nix-nzbr: Build packages using pnpm with nix, but it seems to result in huge Docker images that contain many packages that are not necessary at runtime. As far as I can tell, it just adds every package named in the lockfile and includes them all in a single Nix derivation. It even includes all the tarballs for all the packages as separate Nix derivations.

I think a better design would have a Nix derivation per package. We rely on pnpm to resolve all the dependencies and write the dependency graph to the lockfile, and for each package the node_modules directory will include soft links for both direct and peer dependencies.

I’m pretty new to both Nix and the npm ecosystem and I’ve been relying on Claude Sonnet 4 to help me explore this idea and determine its feasibility, so I wanted to solicit feedback from people who know these areas better before launching into it.

The Problem with Current Approach

The existing pnpm2nix recreates pnpm’s entire .pnpm store structure in one large derivation. This means:

  • Massive Docker images: Everything in the lockfile gets included, even packages only used by dev dependencies or unused transitive dependencies
  • Poor caching: Changing any dependency rebuilds the entire dependency tree
  • No Nix integration: Can’t use overlays, package overrides, or other Nix tooling on individual packages

Proposed Approach: One Derivation Per Package

Instead of one big derivation, create individual derivations for each resolved package context:

  • react@19.1.0 → one derivation
  • some-ui-lib@1.0.0(react@19.1.0)(typescript@5.3.2) → different derivation (peer deps create separate contexts)
  • some-ui-lib@1.0.0(react@18.0.0)(typescript@5.3.2) → yet another derivation

Each derivation would contain the package’s extracted files, and include symlinks to both direct and peer dependencies.

Why This Might Work

Ecosystem resilience: The JavaScript ecosystem already works with diverse package managers (npm flattening, pnpm symlinks, Yarn PnP eliminating node_modules entirely). Packages that work with these should work with our symlink approach.

pnpm already solved the hard parts: The lockfile contains the resolved dependency graph. We don’t need to reimplement dependency resolution - just faithfully recreate what pnpm already figured out.

Content-addressable alignment: Both pnpm and Nix use content-addressable storage. We’re translating between compatible systems.

Nix’s lazy evaluation: Only packages actually reachable from your application would get built and included, automatically excluding unused dependencies.

Benefits

  • Smaller images: Only include packages actually needed
  • Better caching: Individual package changes only rebuild affected derivations
  • Nix integration: Full support for overlays, overrides, etc.
  • Fine-grained layers: Each package becomes a Docker layer with buildLayeredImage

Implementation Approach

  1. Parse pnpm-lock.yaml (using yaml2json)
  2. Treat complex package identifiers like package@1.0.0(react@19.1.0) as opaque strings - we don’t need to understand what the parentheses mean, just use the whole string as a unique package identifier
  3. Create one derivation per unique package identifier
  4. Wire up dependencies based on lockfile relationships
  5. Build symlink structure for each package’s node_modules

Questions for the Community

Is this a good idea? Does this approach make sense from a Nix perspective?

What problems am I going to run into? I can think of a few potential issues:

  • Node.js module resolution behaving differently with symlinks vs the current approach
  • Workspace dependencies creating build ordering complexity
  • Native packages (.node files) having filesystem layout assumptions
  • Path length limits with deep Nix store symlink structures

Has this been tried before? Are there existing approaches I should look at?

Performance concerns? Would having many small derivations instead of one large one create build performance issues?

buildNpmPackage? As far as I can tell, buildNpmPackage doesn’t support pnpm and discussion of how to do so has petered out Add a `buildPnpmPackage` ? Or add pnpm build and install hooks? · Issue #317927 · NixOS/nixpkgs · GitHub but elsewhere it’s suggested this can work, what’s the situation? How to use `buildNpmPackage` for a pnpm project?

Testing strategy? What’s the best way to build automated tests for something like this - both unit and integration tests?

Thanks for any insights!

Hey please have a look into pnpm hooks, about the packages every package that is in the lock file includes packages from other libraries which is required at build time until your application is built with node. After build time your free to delete the packages in your store.

But I feel like the pnpm hook could be improved to where it downloads the packages from nix already via cache and read the lock file, if there is no package then it fetches it from npm.

One benefit is storage space, Performance slightly effected.

1 Like

Yes, and I don’t recall why it didn’t go through. It’s probably a good idea though.

1 Like

I had a look at that page, but AFAICT it’s the opposite of the granularity I want - any time I make any change, everything has to be rebuilt, and all the build-time dependencies as well as the run-time dependencies end up in a single layer in my Docker image unless I delete things by hand. With the solution I propose (combined with eg nix2container), Nix will automatically handle including only what is needed in the Docker image.