I’ve packaged a few pnpm-based projects (including SvelteKit), and the key is leaning on the lockfile rather than fighting it. Using pnpm fetch + a frozen lockfile (--frozen-lockfile) during the build step usually keeps dependencies deterministic. For Nix/Devbox, generating the dependency store ahead of time and treating it as immutable helps a lot.
I also found it useful to look at tooling examples outside the usual Nix docs—sometimes approaches used in build tooling projects translate surprisingly well to pnpm workflows, especially around reproducible installs and caching.
Hope that helps a bit until you find a clean flake setup.
Thanks for the input. What I have come up so far is below.
It does kind of does the job - but the re-build behaviour isn’t quite right yet.
When I change something in the app sources, the app derivation of course changes. But the runtime dependencies are also getting re-evaluated - which should not be necessary unless the package.json and/or lock file changes.
Not to mention - that’s really verbose and would probably be the same for every such project.
OK so it does not look like you need mkRunDeps and mkDevDeps. IIRC, Vite’s build process should handle bundling all external runtime dependencies. If you do need them, your build process is probably messed up, however if there is something i am missing and you truely need this, i would first:
put a single pnpmDeps in the let block and inherit it instead of all fetchPnpmDeps invocations, because they have the same src, hash, etc…
create a factory function that has the common parts of mkRunDeps and mkDevDeps
Indeed. But this separation is not for vite. The idea is to improve the caching.
If the lock file has not changed, the derivations for the runtime or devtime dependencies should not have changed. So the build would only need to run vite to build the final bundle. The dependencies should then come straight from the nix store.
TBH: just packaging all the deps in one derivation that holds the node_modules could also be enough.
What I am after is a fast build.
Am I overcomplicating things?
They basically DO come straight from the nix store, since fetchPnpmDeps builds a cache. pnpm install just symlinks/copies them to node_modules. pnpm install is usually not the bottleneck in most packages. Actually, it looks like your strategy may be slower because pnpmConfigHook runs pnpm install automatically (source).
You can probably remove this section, considering that all node_modules dependencies should be bundled in the built version. You can definitely remove the pnpm install bit, because as i said already pnpmConfigHook runs it already. If you did need this, you would set NODE_PATH in your executable’s node invocation.
pnpmConfigHook basically just does the following:
Extracts the cache produced by fetchPnpmDeps and is available through the pnpmDeps attribute, which is passed through to the derivation environment.
Configures pnpm to use that extracted cache so we don’t try to get things from the network, which fails because nix builds are blocked from network access unless we know the hash of our output.
Runs pnpm install.
Runs patchShebangs on node_modules to make sure things in node_modules/.bin use the right interpreter.
This is basically just standard in Nix builds, we have to copy the results from the ephemeral build sandbox to the store.
Also, please mark an answer as the solution if it solved your problem .
I still think it’s a little much boilerplate code.
Question is how this could be improved further.
And it seems like fetchPnpmDeps does cache, but I don’t feel fully in control. My guess is that any changed flake input causes a re-eval. Although it should (debatable) only depend on the pnpm-lock.yaml
Also, please mark an answer as the solution if it solved your problem
For a Nix flake, this seems like a standard amount of boilerplate. At this point, flake-parts would just be replacing the current boilerplate withflake-parts boilerplate.
Fixed-output derivations (like fetchPnpmDeps) should not rebuild unless the hash is changed, nixpkgs updates, or the pname changes.
FODs do always evaluate, but the derivation hash should be the same and thus skip rebuild.
If you see pnpm install-like logs in the output, it’s probably just installing from the cache produced by fetchPnpmDeps.
This looks basically like a perfect flake for a SvelteKit+pnpm project though, I can’t see anything that can really be improved.
If there was something that I want to control it would be just the node and the pnpm install executions.
I was surprised that (what probably was) a change in nixpkgs triggered a longer eval. The next run was again immediate. But I guess this does make sense. Any input will cause this IIUC.
Since it’s still using the cache it should be alright.
This is just how most Nix things work, you can replace the cat > $out/bin/simple and chmod bit with my makeWrapper invocation from here, and remove runHook invocations if you don’t think people consuming the project’s flake will want to use those hooks. Other than that, you just have to deal with copy+pasting that. It’s really not that bad compared to other nix boilerplate (see: crane boilerplate, crate2nix boilerplate).