{
  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
              (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;
})
