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.jsonmanually (yarn add xxxornpm install xxx). Run locally installed CLI tools withnpx, for examplenpx 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.jsondoes not work as is. It might call a CLI tool that is not available, orcdinto a directory with a differentpackage.json, which is common with workspaces. Read what the script does. Reproduce it in the build phases. For example, abuildscript may callbuild:uiandbuild:serverin turn. If one fails, split them into separate steps.yarn build:ui yarn build:server # OR npm run build:ui npm run build:serverWhen 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 withprefetch-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 totrueif the package does not have a build script. Defaults tofalse. Alternatively, settingbuildPhaseexplicitly also disables this.dontNpmInstall: Option to disable runningnpm install. Defaults tofalse. Alternatively, settinginstallPhaseexplicitly also disables this.- []{#javascript-buildNpmPackage-npmFlags}
npmFlags: Flags to pass to all npm commands. npmInstallFlags: Flags to pass tonpm ci.npmBuildFlags: Flags to pass tonpm run ${npmBuildScript}.npmPackFlags: Flags to pass tonpm pack.npmPruneFlags: Flags to pass tonpm prune. Defaults to the value ofnpmInstallFlags.makeWrapperArgs: Flags to pass tomakeWrapper, added to executable calling the generated.jswithnodeas an interpreter. These scripts are defined inpackage.json.nodejs: Thenodejspackage to build against, using the correspondingnpmshipped with that version ofnode. Defaults topkgs.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 apackage-lock.jsonfilehash: The output hash of the dependencies defined inpackage-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, thepackageandpackageLockarguments must be specified instead.package: Parsed contents ofpackage.jsonpackageLock: Parsed contents ofpackage-lock.jsonpname: Package nameversion: Package versionfetcherOpts: 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.fetchurlforhttp(s)dependenciesfetchGitforgitdependencies
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
packageandpackageLockarguments must both be specified. package(Attrset; optional)- Parsed contents of
package.json, as returned bylib.importJSON ./my-package.json. If not specified, thepackage.jsoninnpmRootis used. packageLock(Attrset; optional)- Parsed contents of
package-lock.json, as returned bylib.importJSON ./my-package-lock.json. If not specified, thepackage-lock.jsoninnpmRootis used. derivationArgs(mkDerivationattrset; 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.
- 1: Initial version, nothing special. (removed in 26.11)
- 2: Ensure consistent permissions (removed in 26.11)
- 3: Build a reproducible tarball
- 4: Dump SQLite database to an SQL file
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 intonode_modules.yarnBuildHook: Runsyarn buildor a specifiedyarncommand that builds the project.yarnInstallHook: Runsyarn install --productionto 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 differentyarn --offlinesubcommand (defaults tobuild).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 fromnode_modulesbefore 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-fetcherfetchYarnBerryDepsyarnBerryConfigHook
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.