{ # System packages. pkgs, # Derivation for nodejs and npm. nodejs, # Which version of npm to use. npm ? nodejs, # List of required native build inputs. neededNatives, # Self-reference for overriding purposes. buildNodePackage, }: let inherit (pkgs) stdenv; inherit (pkgs.lib) showVal; # This expression builds the raw C headers and source files for the base # node.js installation. Node packages which use the C API for node need to # link against these files and use the headers. nodejsSources = pkgs.runCommand "node-sources" {} '' tar --no-same-owner --no-same-permissions -xf ${nodejs.src} mv $(find . -type d -mindepth 1 -maxdepth 1) $out ''; # The path within $out/lib to find a package. If the package does not # have a namespace, it will simply be in `node_modules`, and otherwise it # will appear in `node_modules/@namespace`. modulePath = pkg: if !(builtins.hasAttr "namespace" pkg) then throw '' Package dependency does not appear to be a node package: ${showVal pkg}. Use "buildInputs" or "propagatedBuildInputs" for non-node dependencies, instead of "deps". '' else if pkg.namespace == null then "node_modules" else "node_modules/@${pkg.namespace}"; # The path to the package within its modulePath. Just appending the name # of the package. pathInModulePath = pkg: "${modulePath pkg}/${pkg.basicName}"; # By default, when checking we'll run npm test. defaultCheckPhase = '' runHook preCheck npm test runHook postCheck ''; in { # The name of the package. If it's a private package with a namespace, # this should not contain the namespace. name, # Used for private packages. Indicated in the name field of the # package.json, e.g. if the name in that file is "@mynamespace/mypackage", # then the namespace is 'mynamespace' and the name is 'mypackage'. Public # packages will not need this. namespace ? null, # Version of the package. This should follow the semver standard, although # we don't explicitly enforce that in this function. version, # Source of the package; can be a tarball or a folder on the filesystem. src, # The suffix to the name as it appears in `nix-env -q` and the nix store. By # default, the name of nodejs interpreter e.g: # "--nodejs-" nameSuffix ? "-${nodejs.name}", # If there's a namespace, by default it will be prepended to the package # name. Otherwise, a prefix can be given explicitly. namePrefix ? (if namespace == null then "" else "${namespace}-"), # List or attribute set of (runtime) dependencies. deps ? {}, # List or attribute set of peer dependencies. See: # https://nodejs.org/en/blog/npm/peer-dependencies/ peerDependencies ? {}, # List or attribute set of optional dependencies. optionalDependencies ? {}, # List of optional dependencies to skip. List of strings, where a string # should contain the `name` of the derivation to skip (not a version or # namespace). skipOptionalDependencies ? [], # List or set of development dependencies (or null). These will only be # installed when `installDevDependencies` is true, which is provided by # the `.env` attribute. devDependencies ? null, # If true and devDependencies are defined, the package will only be # installed contingent on successfully running tests. doCheck ? false, # Test command. checkPhase ? defaultCheckPhase, # Additional flags passed to npm install. A list of strings. extraNpmFlags ? [], # Same as https://docs.npmjs.com/files/package.json#os os ? [], # Attribute set of already resolved dependencies, for avoiding infinite # recursion. Used internally (don't use this argument explicitly). resolvedDeps ? {}, # Build inputs to propagate in addition to nodejs and non-dev dependencies. propagatedBuildInputs ? [], # Build inputs in addition to npm and dev dependencies. buildInputs ? [], # Whether to strip debugging symbols from binaries and patch ELF executables. # These should both probably be true but can be overridden. # Doc for details: https://nixos.org/wiki/NixPkgs_Standard_Environment. dontStrip ? true, dontPatchELF ? true, # Optional attributes to pass through to downstream derivations. passthru ? {}, # A set of dependencies to patch. patchDependencies ? {}, # Any remaining flags are passed through to mkDerivation. ... } @ args: let # The package name as it appears in the package.json. This contains a # namespace if there is one, so it will be a distinct identifier for # different packages. packageJsonName = if namespace == null then name else "@${namespace}/${name}"; # The package name with a version appended. This should be unique amongst # all packages. uniqueName = "${packageJsonName}@${version}"; in if doCheck && (devDependencies == null) then throw ("${uniqueName}: Can't run tests because devDependencies have " + "not been defined.") else let inherit (stdenv.lib) fold removePrefix hasPrefix subtractLists isList flip intersectLists isAttrs listToAttrs nameValuePair mapAttrs filterAttrs attrNames elem concatMapStrings attrValues getVersion flatten remove concatStringsSep; dependencyTypes = ["dependencies" "devDependencies" "peerDependencies" "optionalDependencies"]; # These arguments are intended as directives to this function and not # to be passed through to mkDerivation. They are removed below. attrsToRemove = ["deps" "resolvedDeps" "flags" "os" "skipOptionalDependencies" "passthru" "doCheck" "installDevDependencies" "version" "namespace" "patchDependencies"] ++ dependencyTypes; # We create a `self` object for self-referential expressions. It # bottoms out in a call to `mkDerivation` at the end. self = let platforms = if os == [] then nodejs.meta.platforms else fold (entry: platforms: let filterPlatforms = stdenv.lib.platforms.${removePrefix "!" entry} or []; in # Ignore unknown platforms if filterPlatforms == [] then (if platforms == [] then nodejs.meta.platforms else platforms) else if hasPrefix "!" entry then subtractLists (intersectLists filterPlatforms nodejs.meta.platforms) platforms else platforms ++ (intersectLists filterPlatforms nodejs.meta.platforms) ) [] os; strict = x: builtins.seq x x; toAttrSet = obj: if isAttrs obj then obj else (listToAttrs (map (x: nameValuePair x.name x) obj)); mapDependencies = deps: filterFunc: let attrDeps = toAttrSet deps; in rec { # All required node modules, without already resolved dependencies # Also override with already resolved dependencies requiredDeps = strict (mapAttrs (name: dep: dep.overrideNodePackage { resolvedDeps = resolvedDeps // {"${name}" = self;}; } ) (filterAttrs filterFunc (removeAttrs attrDeps (attrNames resolvedDeps)))); # Recursive dependencies that we want to avoid with shim creation recursiveDeps = strict (filterAttrs filterFunc (removeAttrs attrDeps (attrNames requiredDeps))); }; # Filter out self-referential dependencies. _dependencies = strict (mapDependencies deps (name: dep: dep.uniqueName != uniqueName)); # Filter out self-referential peer dependencies. _peerDependencies = strict (mapDependencies peerDependencies (name: dep: dep.uniqueName != uniqueName)); # Filter out any optional dependencies which don't build correctly. _optionalDependencies = strict (mapDependencies optionalDependencies (name: dep: (builtins.tryEval dep).success && !(elem dep.basicName skipOptionalDependencies) )); # Grab development dependencies if doCheck is true. _devDependencies = let filterFunc = name: dep: dep.uniqueName != uniqueName; depSet = if doCheck then devDependencies else []; in strict (mapDependencies depSet filterFunc); # Depencencies we need to propagate (all except devDependencies) propagatedDependencies = strict ( _dependencies.requiredDeps // _optionalDependencies.requiredDeps // _peerDependencies.requiredDeps); # Required dependencies are those that we haven't filtered yet. requiredDependencies = strict (_devDependencies.requiredDeps // propagatedDependencies); # Recursive dependencies. These are turned into "shims" or fake packages, # which allows us to have dependency cycles, something npm allows. recursiveDependencies = strict ( _devDependencies.recursiveDeps // _dependencies.recursiveDeps // _optionalDependencies.recursiveDeps // _peerDependencies.recursiveDeps); # Flags that we will pass to `npm install`. npmFlags = concatStringsSep " " ([ # We point the registry at something that doesn't exist. This will # mean that NPM will fail if any of the dependencies aren't met, as it # will attempt to hit this registry for the missing dependency. "--registry=http://notaregistry.$UNIQNAME.derp" # These flags make failure fast, as otherwise NPM will spin for a while. "--fetch-retry-mintimeout=0" "--fetch-retry-maxtimeout=10" "--fetch-retries=0" # This will disable any user-level npm configuration. "--userconfig=/dev/null" # This flag is used for packages which link against the node headers. "--nodedir=${nodejsSources}" # This will tell npm not to run pre/post publish hooks # "--ignore-scripts" ] ++ # Use the --production flag if we're not running tests; this will make # npm skip the dev dependencies. (if !doCheck then ["--production"] else []) ++ # Add any extra headers that the user has passed in. extraNpmFlags); # A bit of bash to check that variables are set. checkSet = vars: concatStringsSep "\n" (flip map vars (var: '' [[ -z $${var} ]] && { echo "${var} is not set."; exit 1; } '')); patchPhase = '' runHook prePatch patchShebangs $PWD # Ensure that the package name matches what is in the package.json. node ${./checkPackageJson.js} ${packageJsonName} # Remove any impure dependencies from the package.json (see script # for details) node ${./removeImpureDependencies.js} # We do not handle shrinkwraps yet rm npm-shrinkwrap.json 2>/dev/null || true ${if patchDependencies == {} then "" else '' cat <