{ stdenv, runCommand, nodejs, neededNatives}: { name, version ? "", src, # by default name of nodejs interpreter e.g. "nodejs-${name}" namePrefix ? nodejs.interpreterName + "-", # Node package name pkgName ? if version != "" then stdenv.lib.removeSuffix "-${version}" name else (builtins.parseDrvName name).name, # List or attribute set of dependencies deps ? {}, # List or attribute set of peer depencies peerDependencies ? {}, # List or attribute set of optional dependencies optionalDependencies ? {}, # List of optional dependencies to skip skipOptionalDependencies ? [], # Whether package is binary or library bin ? false, # Additional flags passed to npm install flags ? "", # Command to be run before shell hook preShellHook ? "", # Command to be run after shell hook postShellHook ? "", # Same as https://docs.npmjs.com/files/package.json#os os ? [], # Same as https://docs.npmjs.com/files/package.json#cpu cpu ? [], # Attribute set of already resolved deps (internal), # for avoiding infinite recursion resolvedDeps ? {}, ... } @ args: with stdenv.lib; let self = let sources = runCommand "node-sources" {} '' tar --no-same-owner --no-same-permissions -xf ${nodejs.src} mv $(find . -type d -mindepth 1 -maxdepth 1) $out ''; 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 platforms else if hasPrefix "!" entry then subtractLists (intersectLists filterPlatforms nodejs.meta.platforms) platforms else platforms ++ (intersectLists filterPlatforms nodejs.meta.platforms) ) [] os; mapDependencies = deps: f: rec { # Convert deps to attribute set attrDeps = if isAttrs deps then deps else (listToAttrs (map (dep: nameValuePair dep.name dep) deps)); # All required node modules, without already resolved dependencies # Also override with already resolved dependencies requiredDeps = mapAttrs (name: dep: dep.override { resolvedDeps = resolvedDeps // { "${name}" = self; }; } ) (filterAttrs f (removeAttrs attrDeps (attrNames resolvedDeps))); # Recursive dependencies that we want to avoid with shim creation recursiveDeps = filterAttrs f (removeAttrs attrDeps (attrNames requiredDeps)); }; _dependencies = mapDependencies deps (name: dep: dep.pkgName != pkgName); _optionalDependencies = mapDependencies optionalDependencies (name: dep: (builtins.tryEval dep).success && !(elem dep.pkgName skipOptionalDependencies) ); _peerDependencies = mapDependencies peerDependencies (name: dep: dep.pkgName != pkgName); requiredDependencies = _dependencies.requiredDeps // _optionalDependencies.requiredDeps // _peerDependencies.requiredDeps; recursiveDependencies = _dependencies.recursiveDeps // _optionalDependencies.recursiveDeps // _peerDependencies.recursiveDeps; patchShebangs = dir: '' node=`type -p node` coffee=`type -p coffee || true` find -L ${dir} -type f -print0 | xargs -0 grep -Il . | \ xargs sed --follow-symlinks -i \ -e 's@#!/usr/bin/env node@#!'"$node"'@' \ -e 's@#!/usr/bin/env coffee@#!'"$coffee"'@' \ -e 's@#!/.*/node@#!'"$node"'@' \ -e 's@#!/.*/coffee@#!'"$coffee"'@' || true ''; in stdenv.mkDerivation ({ inherit src; configurePhase = '' runHook preConfigure ${patchShebangs "./"} # Some version specifiers (latest, unstable, URLs, file paths) force NPM # to make remote connections or consult paths outside the Nix store. # The following JavaScript replaces these by * to prevent that: # Also some packages require a specific npm version because npm may # resovle dependencies differently, but npm is not used by Nix for dependency # reslution, so these requirements are dropped. ( cat </dev/null || true mkdir ../build-dir ( cd ../build-dir mkdir node_modules # Symlink or copy dependencies for node modules # copy is needed if dependency has recursive dependencies, # because node can't follow symlinks while resolving recursive deps. ${concatMapStrings (dep: if dep.recursiveDeps == [] then '' ln -sv ${dep}/lib/node_modules/${dep.pkgName} node_modules/ '' else '' cp -R ${dep}/lib/node_modules/${dep.pkgName} node_modules/ '' ) (attrValues requiredDependencies)} # Create shims for recursive dependenceies ${concatMapStrings (dep: '' mkdir -p node_modules/${dep.pkgName} cat > node_modules/${dep.pkgName}/package.json </dev/null || true if [ -d "$out/lib/node_modules/.bin" ]; then ln -sv $out/lib/node_modules/.bin $out/bin ${patchShebangs "$out/lib/node_modules/.bin/*"} fi ) runHook postInstall ''; preFixup = '' find $out -type f -print0 | xargs -0 sed -i 's|${src}|${src.name}|g' ''; shellHook = '' ${preShellHook} export PATH=${nodejs}/bin:$(pwd)/node_modules/.bin:$PATH mkdir -p node_modules ${concatMapStrings (dep: '' ln -sfv ${dep}/lib/node_modules/${dep.pkgName} node_modules/ '') (attrValues requiredDependencies)} ${postShellHook} ''; # Stipping does not make a lot of sense in node packages dontStrip = true; meta = { inherit platforms; maintainers = [ stdenv.lib.maintainers.offline ]; }; passthru.pkgName = pkgName; } // (filterAttrs (n: v: all (k: n != k) ["deps" "resolvedDeps" "optionalDependencies"]) args) // { name = namePrefix + name; # Run the node setup hook when this package is a build input propagatedNativeBuildInputs = (args.propagatedNativeBuildInputs or []) ++ [ nodejs ]; nativeBuildInputs = (args.nativeBuildInputs or []) ++ neededNatives ++ (attrValues requiredDependencies); # Expose list of recursive dependencies upstream, up to the package that # caused recursive dependency recursiveDeps = (flatten ( map (dep: remove name dep.recursiveDeps) (attrValues requiredDependencies) )) ++ (attrNames recursiveDependencies); }); in self