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 derivationsome-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
- Parse
pnpm-lock.yaml
(usingyaml2json
) - 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 - Create one derivation per unique package identifier
- Wire up dependencies based on lockfile relationships
- 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!