How to use `buildNpmPackage` for a pnpm project?

Hi!

I’m trying to write a flake to package a project of mine that uses pnpm, this is the project’s structure:

.dir-locals.el
.dockerignore
.envrc
.github/workflows/test.yaml
.gitignore
.gitmodules
Dockerfile
Makefile
README.md
flake.lock
flake.nix
index.ts
package.json
pnpm-lock.yaml
protobuf-files
src/configuration.ts
src/protobuf/moonshine.ts
src/protobuf/swap.ts
src/server.ts
src/server_moonshine.ts
tsconfig.json

And this is the flake.nix I was been able to put together:

# This flake was initially generated by fh, the CLI for FlakeHub (version 0.1.9)
{
  # Flake inputs
  inputs = {
    pre-commit-hooks.url = "github:cachix/pre-commit-hooks.nix";
    flake-schemas.url = "https://flakehub.com/f/DeterminateSystems/flake-schemas/*.tar.gz";

    nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz";
  };

  # Flake outputs that other flakes can use
  outputs =
    {
      self,
      flake-schemas,
      nixpkgs,
      pre-commit-hooks,
    }:
    let
      # Helpers for producing system-specific outputs
      supportedSystems = [
        "x86_64-linux"
        "aarch64-darwin"
        "x86_64-darwin"
        "aarch64-linux"
      ];
      forEachSupportedSystem =
        f:
        nixpkgs.lib.genAttrs supportedSystems (
          system:
          f {
            pkgs = import nixpkgs { inherit system; };
            inherit system;
          }
        );
    in
    {
      # Schemas tell Nix about the structure of your flake's outputs
      schemas = flake-schemas.schemas;

      checks = forEachSupportedSystem (
        {
          system,
          pkgs,
        }:
        {
          pre-commit-check = pre-commit-hooks.lib.${system}.run {
            src = ./.;
            hooks = {
              alejandra.enable = true;
              deadnix.enable = true;
              biome = {
                enable = true;
                excludes = [ "src/protobuf" ];
              };
              yamllint = {
                enable = true;
                entry = "${pkgs.python311Packages.yamllint}/bin/yamllint --no-warnings";
                excludes = [ "pnpm-lock\\.yaml" ];
              };
            };
          };
        }
      );

      packages = forEachSupportedSystem (
        { system, pkgs }:
        {
          default = pkgs.buildNpmPackage {
            pname = "calisto";
            version = "0.1.0";

            src = ./.;
            npmDepsHash = "sha256-hFtcQ1jSlPKqU2OlqjhknkzUkZbnma5FbrBNUc8gDLw=";

            npmInstallFlags = [ "--production" ];
            npmBuildScript = "build";
          };
        }
      );
      # Development environments
      devShells = forEachSupportedSystem (
        {
          pkgs,
          system,
        }:
        {
          default = pkgs.mkShell {
            buildInputs = self.checks.${system}.pre-commit-check.enabledPackages;
            # Pinned packages available in the environment
            packages = with pkgs; [
              curl
              jq
              just

              nodejs-slim
              nodePackages.prettier
              nodePackages.npm
              nodePackages.pnpm
              typescript
              nodePackages.typescript-language-server

              protobuf
              grpcui # cliente gRPC

              self.packages.${system}.default
            ];
          };
        }
      );
    };
}

However I’m met with the following error:

➜  nix log /nix/store/h3gnsxs0wxgcla3pzgmd3xsq9bl6k5ks-calisto-0.1.0-npm-deps.drv
warning: The interpretation of store paths arguments ending in `.drv` recently changed. If this command is now failing try again with '/nix/store/h3gnsxs0wxgcla3pzgmd3xsq9bl6k5ks-calisto-0.1.0-npm-deps.drv^*'
@nix { "action": "setPhase", "phase": "unpackPhase" }
Running phase: unpackPhase
unpacking source archive /nix/store/gm52ma7c2sqg3qbfzxpnk9xjvricfckc-c0k6l9wki90c47xlf0>
source root is c0k6l9wki90c47xlf0j273qbsih3ifgg-source
@nix { "action": "setPhase", "phase": "patchPhase" }
Running phase: patchPhase
@nix { "action": "setPhase", "phase": "updateAutotoolsGnuConfigScriptsPhase" }
Running phase: updateAutotoolsGnuConfigScriptsPhase
@nix { "action": "setPhase", "phase": "configurePhase" }
Running phase: configurePhase
no configure script, doing nothing
@nix { "action": "setPhase", "phase": "buildPhase" }
Running phase: buildPhase

ERROR: The package-lock.json file does not exist!

package-lock.json is required to make sure that npmDepsHash doesn't change
when packages are updated on npm.

Hint: You can copy a vendored package-lock.json file via postPatch.

The documentation does not touch on the topic of NodeJS with pnpm so it is hard to navigate the problem for a solution.

This is the section of the nixpkgs documentation about packaging pnpm projects: Nixpkgs Reference Manual

I don’t understand it in relationship with the flake as I’m not using stdenv.mkDerivation anywhere

EDIT:

Okay, nevermind, I made the following changes:

      packages = forEachSupportedSystem (
        { system, pkgs }:
        {
          default = pkgs.stdenv.mkDerivation {
            pname = "calisto";
            version = "0.1.0";

            src = ./.;

            nativeBuildInputs = [
              pkgs.nodejs
              pkgs.pnpm.configHook
              pkgs.typescript
            ];

            installPhase = ''
              mkdir -p $out/bin
              cp -r dist $out/
              echo '#!${pkgs.bash}/bin/bash' > $out/bin/calisto
              echo '${pkgs.nodejs}/bin/node $out/dist/index.js "$@"' >> $out/bin/calisto
              chmod +x $out/bin/calisto
            '';

            pnpmDeps = pkgs.pnpm.fetchDeps {
              inherit (self.packages.${system}.default) pname version src;
              hash = "sha256-7NgYqOUOMa1xZlTJf1QTLmGl1TI55o58RxpcF+EBOa0=";
            };
          };
        }
      );

And now it looks like it is working

any idea how I can call my script on dist/index.js? I write a shell script with:

echo '#!${pkgs.bash}/bin/bash' > $out/bin/calisto
echo '${pkgs.nodejs}/bin/node $out/dist/index.js "$@"' >> $out/bin/calisto
chmod +x $out/bin/calisto

but calling the command fails:

➜  calisto
node:internal/modules/cjs/loader:1146
  throw err;
  ^

Error: Cannot find module '/home/jorge/code/crypto/calisto/outputs/out/dist/index.js'
    at Module._resolveFilename (node:internal/modules/cjs/loader:1143:15)
    at Module._load (node:internal/modules/cjs/loader:984:27)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)
    at node:internal/main/run_main_module:28:49 {
  code: 'MODULE_NOT_FOUND',
  requireStack: []
}

Node.js v20.12.2

I don’t know how to solve your issue completely but $out is not what you should use in the second line when writing to $out/bin/calisto.
It should be ${placeholder "out"}/dist/index.js which will be replaced at the end by the real nix store path of the final derivation, instead of local $out variable which is different and will only exist during the time of building (which nix does in a sandbox).

Also checkout pkgs.runtimeShell and patchShebangs (patchShebanges only works with files which have executable permission) you may find them useful.