about summary refs log tree commit diff
path: root/nixpkgs/pkgs/build-support/node
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/pkgs/build-support/node')
-rw-r--r--nixpkgs/pkgs/build-support/node/build-npm-package/default.nix17
-rw-r--r--nixpkgs/pkgs/build-support/node/import-npm-lock/default.nix134
-rw-r--r--nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/canonicalize-symlinks.js52
-rw-r--r--nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/default.nix13
-rw-r--r--nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/npm-config-hook.sh70
5 files changed, 283 insertions, 3 deletions
diff --git a/nixpkgs/pkgs/build-support/node/build-npm-package/default.nix b/nixpkgs/pkgs/build-support/node/build-npm-package/default.nix
index 42c6a9c065b2..1c7bf63e8cd6 100644
--- a/nixpkgs/pkgs/build-support/node/build-npm-package/default.nix
+++ b/nixpkgs/pkgs/build-support/node/build-npm-package/default.nix
@@ -49,6 +49,12 @@
   name = "${name}-npm-deps";
   hash = npmDepsHash;
 }
+  # Custom npmConfigHook
+, npmConfigHook ? null
+  # Custom npmBuildHook
+, npmBuildHook ? null
+  # Custom npmInstallHook
+, npmInstallHook ? null
 , ...
 } @ args:
 
@@ -57,14 +63,19 @@ let
   npmHooks = buildPackages.npmHooks.override {
     inherit nodejs;
   };
-
-  inherit (npmHooks) npmConfigHook npmBuildHook npmInstallHook;
 in
 stdenv.mkDerivation (args // {
   inherit npmDeps npmBuildScript;
 
   nativeBuildInputs = nativeBuildInputs
-    ++ [ nodejs npmConfigHook npmBuildHook npmInstallHook nodejs.python ]
+    ++ [
+      nodejs
+      # Prefer passed hooks
+      (if npmConfigHook != null then npmConfigHook else npmHooks.npmConfigHook)
+      (if npmBuildHook != null then npmBuildHook else npmHooks.npmBuildHook)
+      (if npmInstallHook != null then npmInstallHook else npmHooks.npmInstallHook)
+      nodejs.python
+    ]
     ++ lib.optionals stdenv.isDarwin [ darwin.cctools ];
   buildInputs = buildInputs ++ [ nodejs ];
 
diff --git a/nixpkgs/pkgs/build-support/node/import-npm-lock/default.nix b/nixpkgs/pkgs/build-support/node/import-npm-lock/default.nix
new file mode 100644
index 000000000000..d530b8ee30ff
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/node/import-npm-lock/default.nix
@@ -0,0 +1,134 @@
+{ lib
+, fetchurl
+, stdenv
+, callPackages
+, runCommand
+}:
+
+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
+    }: (
+      if module ? "resolved" 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;
+                }
+              )
+            else if lib.hasPrefix "git" module.resolved then
+              (
+                builtins.fetchGit {
+                  url = module.resolved;
+                }
+              )
+            else throw "Unsupported URL scheme: ${scheme}"
+          )
+        )
+      else null
+    );
+
+  # Manage node_modules outside of the store with hooks
+  hooks = callPackages ./hooks { };
+
+in
+{
+  importNpmLock =
+    { npmRoot ? null
+    , package ? importJSON (npmRoot + "/package.json")
+    , packageLock ? importJSON (npmRoot + "/package-lock.json")
+    , pname ? getName package
+    , version ? getVersion package
+    }:
+    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
+            (_: module:
+              let
+                src = fetchModule {
+                  inherit module npmRoot;
+                };
+              in
+              (removeAttrs module [
+                "link"
+                "funding"
+              ]) // 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
+    '';
+
+  inherit hooks;
+  inherit (hooks) npmConfigHook;
+
+  __functor = self: self.importNpmLock;
+}
diff --git a/nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/canonicalize-symlinks.js b/nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/canonicalize-symlinks.js
new file mode 100644
index 000000000000..81cd2593c5b2
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/canonicalize-symlinks.js
@@ -0,0 +1,52 @@
+#!/usr/bin/env node
+const fs = require("fs");
+const path = require("path");
+
+// When installing files rewritten to the Nix store with npm
+// npm writes the symlinks relative to the build directory.
+//
+// This makes relocating node_modules tricky when refering to the store.
+// This script walks node_modules and canonicalizes symlinks.
+
+async function canonicalize(storePrefix, root) {
+  console.log(storePrefix, root)
+  const entries = await fs.promises.readdir(root);
+  const paths = entries.map((entry) => path.join(root, entry));
+
+  const stats = await Promise.all(
+    paths.map(async (path) => {
+      return {
+        path: path,
+        stat: await fs.promises.lstat(path),
+      };
+    })
+  );
+
+  const symlinks = stats.filter((stat) => stat.stat.isSymbolicLink());
+  const dirs = stats.filter((stat) => stat.stat.isDirectory());
+
+  // Canonicalize symlinks to their real path
+  await Promise.all(
+    symlinks.map(async (stat) => {
+      const target = await fs.promises.realpath(stat.path);
+      if (target.startsWith(storePrefix)) {
+        await fs.promises.unlink(stat.path);
+        await fs.promises.symlink(target, stat.path);
+      }
+    })
+  );
+
+  // Recurse into directories
+  await Promise.all(dirs.map((dir) => canonicalize(storePrefix, dir.path)));
+}
+
+async function main() {
+  const args = process.argv.slice(2);
+  const storePrefix = args[0];
+
+  if (fs.existsSync("node_modules")) {
+    await canonicalize(storePrefix, "node_modules");
+  }
+}
+
+main();
diff --git a/nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/default.nix b/nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/default.nix
new file mode 100644
index 000000000000..5990371def91
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/default.nix
@@ -0,0 +1,13 @@
+{ callPackage, lib, makeSetupHook, srcOnly, nodejs }:
+{
+  npmConfigHook = makeSetupHook
+    {
+      name = "npm-config-hook";
+      substitutions = {
+        nodeSrc = srcOnly nodejs;
+        nodeGyp = "${nodejs}/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js";
+        canonicalizeSymlinksScript = ./canonicalize-symlinks.js;
+        storePrefix = builtins.storeDir;
+      };
+    } ./npm-config-hook.sh;
+}
diff --git a/nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/npm-config-hook.sh b/nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/npm-config-hook.sh
new file mode 100644
index 000000000000..35c3a2061d4b
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/node/import-npm-lock/hooks/npm-config-hook.sh
@@ -0,0 +1,70 @@
+# shellcheck shell=bash
+
+npmConfigHook() {
+    echo "Executing npmConfigHook"
+
+    if [ -n "${npmRoot-}" ]; then
+      pushd "$npmRoot"
+    fi
+
+    if [ -z "${npmDeps-}" ]; then
+        echo "Error: 'npmDeps' should be set when using npmConfigHook."
+        exit 1
+    fi
+
+    echo "Configuring npm"
+
+    export HOME="$TMPDIR"
+    export npm_config_nodedir="@nodeSrc@"
+    export npm_config_node_gyp="@nodeGyp@"
+    npm config set offline true
+    npm config set progress false
+    npm config set fund false
+
+    echo "Installing patched package.json/package-lock.json"
+
+    # Save original package.json/package-lock.json for closure size reductions.
+    # The patched one contains store paths we don't want at runtime.
+    mv package.json .package.json.orig
+    if test -f package-lock.json; then # Not all packages have package-lock.json.
+        mv package-lock.json .package-lock.json.orig
+    fi
+    cp --no-preserve=mode "${npmDeps}/package.json" package.json
+    cp --no-preserve=mode "${npmDeps}/package-lock.json" package-lock.json
+
+    echo "Installing dependencies"
+
+    if ! npm install --ignore-scripts $npmInstallFlags "${npmInstallFlagsArray[@]}" $npmFlags "${npmFlagsArray[@]}"; then
+        echo
+        echo "ERROR: npm failed to install dependencies"
+        echo
+        echo "Here are a few things you can try, depending on the error:"
+        echo '1. Set `npmFlags = [ "--legacy-peer-deps" ]`'
+        echo
+
+        exit 1
+    fi
+
+    patchShebangs node_modules
+
+    npm rebuild $npmRebuildFlags "${npmRebuildFlagsArray[@]}" $npmFlags "${npmFlagsArray[@]}"
+
+    patchShebangs node_modules
+
+    # Canonicalize symlinks from relative paths to the Nix store.
+    node @canonicalizeSymlinksScript@ @storePrefix@
+
+    # Revert to pre-patched package.json/package-lock.json for closure size reductions
+    mv .package.json.orig package.json
+    if test -f ".package-lock.json.orig"; then
+        mv .package-lock.json.orig package-lock.json
+    fi
+
+    if [ -n "${npmRoot-}" ]; then
+      popd
+    fi
+
+    echo "Finished npmConfigHook"
+}
+
+postConfigureHooks+=(npmConfigHook)