{ lib, fetchurl, stdenv, callPackages, runCommand, cctools, }: let inherit (builtins) match elemAt toJSON removeAttrs ; inherit (lib) importJSON mapAttrs; matchGitHubReference = match "github(.com)?:.+"; getName = package: package.name or "unknown"; getVersion = package: package.version or "0.0.0"; # Fetch a module from package-lock.json -> packages fetchModule = { module, npmRoot ? null, fetcherOpts, }: ( if module ? "resolved" && module.resolved != null then ( let # Parse scheme from URL mUrl = match "(.+)://(.+)" module.resolved; scheme = elemAt mUrl 0; in ( if mUrl == null then ( assert npmRoot != null; { outPath = npmRoot + "/${module.resolved}"; } ) else if (scheme == "http" || scheme == "https") then (fetchurl ( { url = module.resolved; hash = module.integrity; } // fetcherOpts )) else if lib.hasPrefix "git" module.resolved then (builtins.fetchGit ( { url = module.resolved; } // fetcherOpts )) else throw "Unsupported URL scheme: ${scheme}" ) ) else null ); cleanModule = lib.flip removeAttrs [ "link" # Remove link not to symlink directories. These have been processed to store paths already. "funding" # Remove funding to get rid sponsorship nag in build output ]; # Manage node_modules outside of the store with hooks hooks = callPackages ./hooks { }; in lib.fix (self: { importNpmLock = { npmRoot ? null, package ? importJSON (npmRoot + "/package.json"), packageLock ? importJSON (npmRoot + "/package-lock.json"), pname ? getName package, version ? getVersion package, # A map of additional fetcher options forwarded to the fetcher used to download the package. # Example: { "node_modules/axios" = { curlOptsList = [ "--verbose" ]; }; } # This will download the axios package with curl's verbose option. fetcherOpts ? { }, # A map from node_module path to an alternative package to use instead of fetching the source in package-lock.json. # Example: { "node_modules/axios" = stdenv.mkDerivation { ... }; } # This is useful if you want to inject custom sources for a specific package. packageSourceOverrides ? { }, }: let mapLockDependencies = mapAttrs ( name: version: ( # Substitute the constraint with the version of the dependency from the top-level of package-lock. if ( # if the version is `latest` version == "latest" || # Or if it's a github reference matchGitHubReference version != null ) then packageLock'.packages.${"node_modules/${name}"}.version # But not a regular version constraint else version ) ); packageLock' = packageLock // { packages = mapAttrs ( modulePath: module: let src = packageSourceOverrides.${modulePath} or (fetchModule { inherit module npmRoot; fetcherOpts = fetcherOpts.${modulePath} or { }; }); in cleanModule module // lib.optionalAttrs (src != null) { resolved = "file:${src}"; } // lib.optionalAttrs (module ? dependencies) { dependencies = mapLockDependencies module.dependencies; } // lib.optionalAttrs (module ? optionalDependencies) { optionalDependencies = mapLockDependencies module.optionalDependencies; } ) packageLock.packages; }; mapPackageDependencies = mapAttrs ( name: _: packageLock'.packages.${"node_modules/${name}"}.resolved ); # Substitute dependency references in package.json with Nix store paths packageJSON' = package // lib.optionalAttrs (package ? dependencies) { dependencies = mapPackageDependencies package.dependencies; } // lib.optionalAttrs (package ? devDependencies) { devDependencies = mapPackageDependencies package.devDependencies; }; pname = package.name or "unknown"; in runCommand "${pname}-${version}-sources" { inherit pname version; passAsFile = [ "package" "packageLock" ]; package = toJSON packageJSON'; packageLock = toJSON packageLock'; } '' mkdir $out cp "$packagePath" $out/package.json cp "$packageLockPath" $out/package-lock.json ''; # Build node modules from package.json & package-lock.json buildNodeModules = { npmRoot ? null, package ? importJSON (npmRoot + "/package.json"), packageLock ? importJSON (npmRoot + "/package-lock.json"), nodejs, derivationArgs ? { }, }: stdenv.mkDerivation ( { pname = derivationArgs.pname or "${getName package}-node-modules"; version = derivationArgs.version or getVersion package; dontUnpack = true; npmDeps = self.importNpmLock { inherit npmRoot package packageLock; }; package = toJSON package; packageLock = toJSON packageLock; installPhase = '' runHook preInstall mkdir $out cp package.json $out/ cp package-lock.json $out/ [[ -d node_modules ]] && mv node_modules $out/ runHook postInstall ''; } // derivationArgs // { nativeBuildInputs = [ nodejs nodejs.passthru.python hooks.npmConfigHook ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ cctools ] ++ derivationArgs.nativeBuildInputs or [ ]; passAsFile = [ "package" "packageLock" ] ++ derivationArgs.passAsFile or [ ]; postPatch = '' cp --no-preserve=mode "$packagePath" package.json cp --no-preserve=mode "$packageLockPath" package-lock.json '' + derivationArgs.postPatch or ""; } ); inherit hooks; inherit (hooks) npmConfigHook linkNodeModulesHook; __functor = self: self.importNpmLock; })