Files
nixpkgs/doc/languages-frameworks/javascript.section.md
2026-07-28 19:26:47 +01:00

26 KiB

JavaScript

Introduction

Package JavaScript applications with the tools below.

Tools overview

General principles

The principles below are ordered by importance.

Use the project's Node.js version

It is often not documented which Node.js version the project uses, but if it is, use the same version when packaging.

This can be a problem if the project uses the latest and greatest and you are trying to use an earlier version of Node.js. Some cryptic errors regarding V8 may appear.

Use the project's package manager and lock file

A lock file (package-lock.json, yarn.lock...) is supposed to make reproducible installations of node_modules for each tool.

Package manager guidelines recommend committing those lock files to the repository. If a particular lock file is present, it is a strong indication of which package manager the project uses.

Use a Nix tool that understands the lock file. Using a different tool might give you a hard-to-understand error because different packages have been installed.

Using a different tool forces you to commit a lock file to the repository. These files are fairly large, so when packaging for nixpkgs, this approach does not scale well.

Exceptions to this rule are:

  • When you encounter one of the bugs from a Nix tool. In each of the tool-specific instructions, known problems are detailed. If a tool has a problem, try another. You may have to re-create a lock file and commit it to Nixpkgs.
  • Some lock files contain a particular version of a package that has been pulled off npm for some reason. In that case, you can recreate the lock file (by removing the original and running npm install, yarn, etc.) and commit this to Nixpkgs.

Use the project's package.json

Exceptions to this rule are:

  • Sometimes the project assumes some dependencies are installed globally. Add them to the package.json manually (yarn add xxx or npm install xxx). Run locally installed CLI tools with npx, for example npx postcss. That is how you call them in the phases.

  • Sometimes there is a version conflict between some dependency requirements. In that case you can fix a version by removing the ^.

  • Sometimes a script in package.json does not work as is. It might call a CLI tool that is not available, or cd into a directory with a different package.json, which is common with workspaces. Read what the script does. Reproduce it in the build phases. For example, a build script may call build:ui and build:server in turn. If one fails, split them into separate steps.

    yarn build:ui
    yarn build:server
    # OR
    npm run build:ui
    npm run build:server
    

    When you need to override package.json, it is best to use the one from the project and make explicit overrides. Here is an example:

    {
      patchedPackageJSON = final.runCommand "package.json" { } ''
        ${jq}/bin/jq '.version = "0.4.0" |
          .devDependencies."@jsdoc/cli" = "^0.2.5"
          ${sonar-src}/package.json > $out
      '';
    }
    

    You still need to commit the modified version of the lock files, but at least the overrides are explicit for everyone to see.

Use node_modules directly

Each tool has an abstraction to build the node_modules (dependencies) directory. You can always use the stdenv.mkDerivation with the node_modules to build the package (symlink the node_modules directory and then use the package build command). The node_modules abstraction can also be used to build some web framework frontends. For an example of this, see how plausible is built. Then, when building the frontend, you can symlink the node_modules directory.

Tool-specific instructions

buildNpmPackage

buildNpmPackage packages npm-based projects in Nixpkgs without the use of an auto-generated dependencies file. It uses npm's cache. It builds a reproducible cache of the project's dependencies and points npm at it.

Here's an example:

{
  lib,
  buildNpmPackage,
  fetchFromGitHub,
}:

buildNpmPackage (finalAttrs: {
  pname = "flood";
  version = "4.7.0";

  src = fetchFromGitHub {
    owner = "jesec";
    repo = "flood";
    tag = "v${finalAttrs.version}";
    hash = "sha256-BR+ZGkBBfd0dSQqAvujsbgsEPFYw/ThrylxUbOksYxM=";
  };

  npmDepsHash = "sha256-tuEfyePwlOy2/mOPdXbqJskO6IowvAP4DWg8xSZwbJw=";

  # The prepack script runs the build script, which we'd rather do in the build phase.
  npmPackFlags = [ "--ignore-scripts" ];

  NODE_OPTIONS = "--openssl-legacy-provider";

  meta = {
    description = "Modern web UI for various torrent clients with a Node.js backend and React frontend";
    homepage = "https://flood.js.org";
    license = lib.licenses.gpl3Only;
    maintainers = with lib.maintainers; [ winter ];
  };
})

In the default installPhase set by buildNpmPackage, it uses npm pack --json --dry-run to decide what files to install. They go in $out/lib/node_modules/$name/, where $name is the name string in the package's package.json. Additionally, the bin and man keys in the source's package.json are used to decide what binaries and manpages are supposed to be installed. If these are not defined, npm pack may miss some files, and no binaries are produced.

Arguments

  • npmDepsHash: The output hash of the dependencies for this project. Can be calculated in advance with prefetch-npm-deps.
  • makeCacheWritable: Whether to make the cache writable prior to installing dependencies. Don't set this unless npm tries to write to the cache directory, as it can slow down the build.
  • npmBuildScript: The script to run to build the project. Defaults to "build".
  • []{#javascript-buildNpmPackage-npmWorkspace} npmWorkspace: The workspace directory within the project to build and install.
  • dontNpmBuild: Option to disable running the build script. Set to true if the package does not have a build script. Defaults to false. Alternatively, setting buildPhase explicitly also disables this.
  • dontNpmInstall: Option to disable running npm install. Defaults to false. Alternatively, setting installPhase explicitly also disables this.
  • []{#javascript-buildNpmPackage-npmFlags} npmFlags: Flags to pass to all npm commands.
  • npmInstallFlags: Flags to pass to npm ci.
  • npmBuildFlags: Flags to pass to npm run ${npmBuildScript}.
  • npmPackFlags: Flags to pass to npm pack.
  • npmPruneFlags: Flags to pass to npm prune. Defaults to the value of npmInstallFlags.
  • makeWrapperArgs: Flags to pass to makeWrapper, added to executable calling the generated .js with node as an interpreter. These scripts are defined in package.json.
  • nodejs: The nodejs package to build against, using the corresponding npm shipped with that version of node. Defaults to pkgs.nodejs.
  • npmDeps: The dependencies used to build the npm package. Especially useful to not have to recompute workspace dependencies.

prefetch-npm-deps

prefetch-npm-deps is a Nixpkgs package that calculates the hash of the dependencies of an npm project ahead of time.

$ ls
package.json package-lock.json index.js
$ prefetch-npm-deps package-lock.json
...
sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

fetchNpmDeps

fetchNpmDeps is a Nix function that requires the following mandatory arguments:

  • src: A directory or tarball with a package-lock.json file
  • hash: The output hash of the dependencies defined in package-lock.json.

It returns a derivation with all package-lock.json dependencies downloaded into $out/, usable as an npm cache.

importNpmLock

This function replaces the npm dependency references in package.json and package-lock.json with paths to the Nix store. How each dependency is fetched can be customized with the fetcherOpts argument.

This is a simpler and more convenient alternative to fetchNpmDeps for managing npm dependencies in Nixpkgs. There is no need to specify a hash, since it relies entirely on the integrity hashes already present in the package-lock.json file.

Inputs
  • npmRoot: Path to the package directory containing the source tree. If this is omitted, the package and packageLock arguments must be specified instead.
  • package: Parsed contents of package.json
  • packageLock: Parsed contents of package-lock.json
  • pname: Package name
  • version: Package version
  • fetcherOpts: An attribute set of arguments forwarded to the underlying fetcher.

It returns a derivation with a patched package.json and package-lock.json with all dependencies resolved to Nix store paths.

:::{.note} npmHooks.npmConfigHook cannot be used with importNpmLock. Use importNpmLock.npmConfigHook instead. :::

:::{.example}

pkgs.importNpmLock usage example
{ buildNpmPackage, importNpmLock }:

buildNpmPackage {
  pname = "hello";
  version = "0.1.0";
  src = ./.;

  npmDeps = importNpmLock { npmRoot = ./.; };

  npmConfigHook = importNpmLock.npmConfigHook;
}

:::

:::{.example}

pkgs.importNpmLock usage example with fetcherOpts

importNpmLock uses the following fetchers:

  • pkgs.fetchurl for http(s) dependencies
  • fetchGit for git dependencies

It is possible to provide additional arguments to individual fetchers as needed:

{ buildNpmPackage, importNpmLock }:

buildNpmPackage {
  pname = "hello";
  version = "0.1.0";
  src = ./.;

  npmDeps = importNpmLock {
    npmRoot = ./.;
    fetcherOpts = {
      # Pass 'curlOptsList' to 'pkgs.fetchurl' while fetching 'axios'
      "node_modules/axios" = {
        curlOptsList = [ "--verbose" ];
      };
    };
  };

  npmConfigHook = importNpmLock.npmConfigHook;
}

:::

importNpmLock.buildNodeModules

importNpmLock.buildNodeModules returns a derivation with a pre-built node_modules directory, as imported by importNpmLock.

This is to be used together with importNpmLock.hooks.linkNodeModulesHook to support nix-shell/nix develop development workflows.

It accepts an argument with the following attributes:

npmRoot (Path; optional)
Path to the package directory containing the source tree. If not specified, the package and packageLock arguments must both be specified.
package (Attrset; optional)
Parsed contents of package.json, as returned by lib.importJSON ./my-package.json. If not specified, the package.json in npmRoot is used.
packageLock (Attrset; optional)
Parsed contents of package-lock.json, as returned by lib.importJSON ./my-package-lock.json. If not specified, the package-lock.json in npmRoot is used.
derivationArgs (mkDerivation attrset; optional)
Arguments passed to stdenv.mkDerivation

For example:

pkgs.mkShell {
  packages = [
    importNpmLock.hooks.linkNodeModulesHook
    nodejs
  ];

  npmDeps = importNpmLock.buildNodeModules {
    npmRoot = ./.;
    inherit nodejs;
  };
}

creates a development shell where a node_modules directory is created and packages are symlinked to the Nix store when activated.

:::{.note} Commands like npm install and npm add that write packages and executables need to be used with --package-lock-only.

This means npm installs dependencies by writing into package-lock.json without modifying the node_modules folder. It installs by reloading the devShell. This gives the nix shell near-exclusive ownership over your node_modules folder.

Set package-lock-only = true in your project-local .npmrc. :::

corepack

This package puts the corepack wrappers for pnpm and yarn in your PATH, and they honor the packageManager setting in the package.json.

pnpm

pnpm is available as the top-level package pnpm. Additionally, there are variants pinned to certain major versions, like pnpm_9, pnpm_10, pnpm_10_29_2 and pnpm_11, which support different sets of lock file versions.

When packaging an application that includes a pnpm-lock.yaml, you need to fetch the pnpm store for that project using a fixed-output-derivation. The function fetchPnpmDeps can create this pnpm store derivation. In conjunction, the setup hook pnpmConfigHook prepares the build environment to install the pre-fetched dependencies store. The example below uses the fetcher and setup hook for a package that has package.json and pnpm-lock.yaml:

There is also the pnpmBuildHook for building packages with pnpm, as seen in .

{
  fetchPnpmDeps,
  nodejs,
  pnpm_11,
  pnpmConfigHook,
  stdenv,
}:
let
  # It is recommended to pin pnpm to a major version, due to regular breaking changes in the store format
  # The latest major version is always available under `pkgs.pnpm`
  # Optionally override pnpm to use a custom nodejs version
  # Make sure that the same nodejs version is referenced in nativeBuildInputs
  # pnpm = pnpm_11.override { nodejs = nodejs_24; };
  pnpm = pnpm_11;
in
stdenv.mkDerivation (finalAttrs: {
  pname = "foo";
  version = "0-unstable-1980-01-01";

  src = {
    #...
  };

  nativeBuildInputs = [
    nodejs # in case scripts are run outside of a pnpm call
    pnpmConfigHook
    pnpm # At least required by pnpmConfigHook, if not other (custom) phases
  ];

  pnpmDeps = fetchPnpmDeps {
    inherit (finalAttrs) pname version src;
    inherit pnpm;
    fetcherVersion = 4;
    hash = "...";
  };
})

Use a pinned version of pnpm (for example pnpm_9 or pnpm_10) to increase reproducibility. An older version may be required if the package needs a certain lock file version. To do so, pass the pnpm argument to fetchPnpmDeps. Then override the pnpm arg in pnpmConfigHook. Here are the changes in the example above to use a pinned pnpm version:

 {
   fetchPnpmDeps,
   nodejs,
-  pnpm,
+  pnpm_10,
   pnpmConfigHook,
   stdenv,
 }:
+let
+  # Optionally override pnpm to use a custom nodejs version
+  # Make sure that the same nodejs version is referenced in nativeBuildInputs
+  # pnpm = pnpm_10.override { nodejs-slim = nodejs-slim_22; };
+in
 stdenv.mkDerivation (finalAttrs: {
   pname = "foo";
   version = "0-unstable-1980-01-01";

   src = {
     #...
   };

   nativeBuildInputs = [
     nodejs # in case scripts are run outside of a pnpm call
     pnpmConfigHook
-    pnpm # At least required by pnpmConfigHook, if not other (custom) phases
+    pnpm_10 # At least required by pnpmConfigHook, if not other (custom) phases
   ];

   pnpmDeps = fetchPnpmDeps {
     inherit (finalAttrs) pname version src;
+    pnpm = pnpm_10;
     fetcherVersion = 4;
     hash = "...";
   };
 })

In case you are patching package.json or pnpm-lock.yaml, make sure to pass finalAttrs.patches to the function as well (i.e., inherit (finalAttrs) patches).

pnpmConfigHook supports adding additional pnpm install flags via pnpmInstallFlags which can be set to a Nix string array:

{
  # ...
  pnpmDeps = fetchPnpmDeps {
    # ...
    inherit (finalAttrs) pnpmInstallFlags;
  };

  pnpmInstallFlags = [ "--shamefully-hoist" ];
}

If needed, set dontPnpmConfigure = true; to fully disable pnpmConfigHook without removing it from inputs manually.

Dealing with sourceRoot

If the pnpm project is in a subdirectory, you can define sourceRoot or setSourceRoot for fetchPnpmDeps. If sourceRoot is different between the parent derivation and fetchPnpmDeps, you have to set pnpmRoot to effectively be the same location as it is in fetchPnpmDeps.

Assuming the directory structure below, you can define sourceRoot and pnpmRoot:

.
├── frontend
│   ├── ...
│   ├── package.json
│   └── pnpm-lock.yaml
└── ...
{
  # ...
  pnpmDeps = fetchPnpmDeps {
    # ...
    sourceRoot = "${finalAttrs.src.name}/frontend";
  };

  # by default the working directory is the extracted source
  pnpmRoot = "frontend";
}

pnpm workspaces

For a pnpm workspace, set pnpmWorkspaces = [ "<workspace project name 1>" "<workspace project name 2>" ] in your fetchPnpmDeps call. pnpm then installs only the dependencies for those workspace packages.

For example:

{
  # ...
  pnpmWorkspaces = [ "@astrojs/language-server" ];
  pnpmDeps = fetchPnpmDeps {
    #...
    inherit (finalAttrs) pnpmWorkspaces;
  };
}

The above would make fetchPnpmDeps call only install dependencies for the @astrojs/language-server workspace package. You do not need to set sourceRoot to make this work.

For these projects, build with pnpm --filter=<pnpm workspace name> build, because npmHooks.npmBuildHook may not work. The example below fits most workspace projects:

{
  buildPhase = ''
    runHook preBuild

    pnpm --filter=@astrojs/language-server build

    runHook postBuild
  '';
}

Additional pnpm commands and settings

If you require setting an additional pnpm configuration setting (such as dedupe-peer-dependents or similar), set prePnpmInstall to the right commands to run. For example:

{
  prePnpmInstall = ''
    pnpm config set dedupe-peer-dependents false
  '';
  pnpmDeps = fetchPnpmDeps {
    inherit (finalAttrs) prePnpmInstall;
    # ...
  };
}

In this example, prePnpmInstall runs in both pnpmConfigHook and the fetchPnpmDeps builder.

pnpm fetcherVersion

This is the version of the output of fetchPnpmDeps. Use 4 for new packages:

{
  # ...
  pnpmDeps = fetchPnpmDeps {
    # ...
    fetcherVersion = 4;
    hash = "..."; # clear this hash and generate a new one
  };
}

When upgrading to a newer fetcherVersion, you need to regenerate the hash.

This variable ensures that we can make changes to the output of fetchPnpmDeps without breaking existing hashes. Changes can include workarounds or bug fixes to existing pnpm issues.

Version history

Version 3 is the minimum supported value. Versions 1 and 2 were removed in the 26.11 release; packages that still use them fail to evaluate and must migrate to fetcherVersion = 3 (or later) and regenerate their hashes.

Yarn

Yarn-based projects use a yarn.lock file instead of a package-lock.json to pin dependencies.

To package Yarn-based applications, you need to distinguish by the version pointers in the yarn.lock file. See the following sections.

Yarn v1

Yarn v1 lockfiles contain a comment # yarn lockfile v1 at the beginning of the file.

Nixpkgs provides the Nix function fetchYarnDeps which fetches an offline cache suitable for running yarn install before building the project. In addition, Nixpkgs provides the hooks:

  • yarnConfigHook: Fetches the dependencies from the offline cache and installs them into node_modules.
  • yarnBuildHook: Runs yarn build or a specified yarn command that builds the project.
  • yarnInstallHook: Runs yarn install --production to prune dependencies and installs the project into $out.

An example usage of the above attributes is:

{
  lib,
  stdenv,
  fetchFromGitHub,
  fetchYarnDeps,
  yarnConfigHook,
  yarnBuildHook,
  yarnInstallHook,
  nodejs,
}:

stdenv.mkDerivation (finalAttrs: {
  pname = "...";
  version = "...";

  src = fetchFromGitHub {
    owner = "...";
    repo = "...";
    tag = "v${finalAttrs.version}";
    hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  };

  yarnOfflineCache = fetchYarnDeps {
    yarnLock = finalAttrs.src + "/yarn.lock";
    hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  };

  nativeBuildInputs = [
    yarnConfigHook
    yarnBuildHook
    yarnInstallHook
    # Needed for executing package.json scripts
    nodejs
  ];

  meta = {
    # ...
  };
})
yarnConfigHook arguments

By default, yarnConfigHook relies upon the attribute ${yarnOfflineCache} (or ${offlineCache} if the former is not set) to find the location of the offline cache produced by fetchYarnDeps. To disable this phase, you can set dontYarnInstallDeps = true or override the configurePhase.

yarnBuildHook arguments

This script by default runs yarn --offline build, and it relies upon the project's dependencies installed at node_modules. Below is a list of additional mkDerivation arguments read by this hook:

  • yarnBuildScript: Sets a different yarn --offline subcommand (defaults to build).
  • yarnBuildFlags: Single string list of additional flags to pass the above command, or a Nix list of such additional flags.
yarnInstallHook arguments

To install the package, yarnInstallHook uses both npm and yarn to clean up project files and dependencies. To disable this phase, you can set dontYarnInstall = true or override the installPhase. Below is a list of additional mkDerivation arguments read by this hook:

  • yarnKeepDevDeps: Disables the removal of devDependencies from node_modules before installation.

Yarn Berry v3/v4

Yarn Berry (v3 / v4) versions have similar formats. They start with blocks like these:

__metadata:
  version: 6
  cacheKey: 8[cX]
__metadata:
  version: 8
  cacheKey: 10[cX]

For these packages, we have some helpers exposed under the respective yarn-berry_3 and yarn-berry_4 packages:

  • yarn-berry-fetcher
  • fetchYarnBerryDeps
  • yarnBerryConfigHook

Explicitly pin the major version. For example, capture the yarn-berry_Xn argument and re-define it as a yarn-berry let binding.

{
  stdenv,
  nodejs,
  yarn-berry_4,
}:

let
  yarn-berry = yarn-berry_4;

in
stdenv.mkDerivation (finalAttrs: {
  pname = "foo";
  version = "0-unstable-1980-01-01";

  src = {
    #...
  };

  nativeBuildInputs = [
    nodejs
    yarn-berry.yarnBerryConfigHook
  ];

  offlineCache = yarn-berry.fetchYarnBerryDeps {
    inherit (finalAttrs) src;
    hash = "...";
  };
})
yarn-berry_X.fetchYarnBerryDeps

fetchYarnBerryDeps runs yarn-berry-fetcher fetch in a fixed-output-derivation. It is a custom fetcher designed to reproducibly download all files in the yarn.lock file, validating their hashes in the process. For git dependencies, it creates a checkout at ${offlineCache}/checkouts/<40-character-commit-hash> (relying on the git commit hash to describe the contents of the checkout).

To produce the hash argument for the fetchYarnBerryDeps call, run yarn-berry-fetcher prefetch:

$ yarn-berry-fetcher prefetch </path/to/yarn.lock> [/path/to/missing-hashes.json]

This prints the hash to stdout. Use it in update scripts to recalculate the hash for a new yarn.lock.

yarn-berry_X.yarnBerryConfigHook

yarnBerryConfigHook uses the store path offlineCache points to, to run a yarn install during the build, producing a usable node_modules directory from the downloaded dependencies.

Internally, this uses a patched version of Yarn to ensure git dependencies are re-packed and any attempted downloads fail immediately.

Patching the project's package.json or yarn.lock files

In case patching the project's package.json or yarn.lock is needed, it's important to pass finalAttrs.patches to fetchYarnBerryDeps as well, so the patched variants are picked up (i.e., inherit (finalAttrs) patches).

Missing hashes in the yarn.lock file

Unfortunately, yarn.lock files do not include hashes for optional/platform-specific dependencies. This is by design.

To compensate for this, run the yarn-berry-fetcher missing-hashes subcommand to produce all missing hashes. These are stored in a missing-hashes.json file, which needs to be passed to both the build itself, as well as the fetchYarnBerryDeps helper:

{
  stdenv,
  nodejs,
  yarn-berry_4,
}:

let
  yarn-berry = yarn-berry_4;

in
stdenv.mkDerivation (finalAttrs: {
  pname = "foo";
  version = "0-unstable-1980-01-01";

  src = {
    #...
  };

  nativeBuildInputs = [
    nodejs
    yarn-berry.yarnBerryConfigHook
  ];

  missingHashes = ./missing-hashes.json;
  offlineCache = yarn-berry.fetchYarnBerryDeps {
    inherit (finalAttrs) src missingHashes;
    hash = "...";
  };
})

Outside Nixpkgs

There are some other tools available, which are written in the Nix language. These can't be used inside Nixpkgs because they require Import From Derivation, which is not allowed in Nixpkgs.

If you are packaging something outside Nixpkgs, consider the following:

npmlock2nix

npmlock2nix aims at building node_modules without code generation. It hasn't reached v1 yet; the API may change.

Pitfalls

There are some problems with npm v7.

nix-npm-buildpackage

nix-npm-buildpackage aims at building node_modules without code generation. It hasn't reached v1 yet; the API may change. It supports both package-lock.json and yarn.lock.

Pitfalls

There are some problems with npm v7.