Skip to content

Commit

Permalink
Add support for pnpm workspaces as single derivation
Browse files Browse the repository at this point in the history
Allow a caller to specify a workspace and a list of components rather
than a single source directory. This allows for the building of pnpm
monorepo projects with dependency links between the various projects and
a single pnpm-lock.yaml for the whole workspace. Requires that the
script has the same name in all components.

The location of all of the per-component package.json files is
overridable. A workspace project can still specify the location of the
root package.json via packageJSON, as well as the components'
package.json files via componentPackageJSONs. The list defaults to
c/package.json for c in components.

Similarly, the list of distDirs for the components is overridable. For a
workspace, the distDirs are all subdirs of $out rather than dist
becoming $out. The default list is c/dist for c in components. Allowing
for override handles the case where one component is built by a
different tool like next and produces a different directory like ".next"

NB: Any "link:" dependencies will need to be recreated as a preBuild
step. pnpm will create the symlinks during install, and nix removes them
because when the node_modules derivation is built, they are dangling.

Attempts to leave the non-workspace pnpm package support intact.
  • Loading branch information
iteratee committed Feb 27, 2024
1 parent ccbd38a commit a17fd10
Showing 1 changed file with 89 additions and 18 deletions.
107 changes: 89 additions & 18 deletions derivation.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{ lib
, stdenv
, nodejs
, rsync
, pkg-config
, callPackage
, writeText
Expand All @@ -15,15 +16,23 @@ let
in
{
mkPnpmPackage =
{ src
{ workspace ? null
, components ? []
, src ? if (workspace != null && components != []) then workspace else null
, packageJSON ? src + "/package.json"
, componentPackageJSONs ? map (c: {
name = "${c}/package.json";
value = src + "/${c}/package.json";
}) components
, pnpmLockYaml ? src + "/pnpm-lock.yaml"
, pnpmWorkspaceYaml ? (if workspace == null then null else workspace + "/pnpm-workspace.yaml")
, pname ? (fromJSON (readFile packageJSON)).name
, version ? (fromJSON (readFile packageJSON)).version or null
, name ? if version != null then "${pname}-${version}" else pname
, registry ? "https://registry.npmjs.org"
, script ? "build"
, distDir ? "dist"
, distDirs ? (if workspace == null then [distDir] else (map (c: "${c}/dist") components))
, installInPlace ? false
, installEnv ? { }
, buildEnv ? { }
Expand All @@ -38,17 +47,65 @@ in
, ...
}@attrs:
let
# Flag that can be computed from arguments, indicating a workspace was
# supplied. Only used in these let bindings.
isWorkspace = workspace != null && components != [];
# Utility functions
forEachConcat = f: xs: concatStringsSep "\n" (map f xs);
forEachComponent = f: forEachConcat f components;
# Computed values used below that don't loop
nativeBuildInputs = [
nodejs
pnpm
pkg-config
] ++ extraBuildInputs;
] ++ extraBuildInputs ++ (optional copyNodeModules rsync);
copyLink =
if copyNodeModules
then "rsync -a --chmod=u+w"
else "ln -s";
rsyncSlash = optionalString copyNodeModules "/";
packageFilesWithoutLockfile =
[
{ name = "package.json"; value = packageJSON; }
] ++ componentPackageJSONs ++ computedNodeModuleSources;
computedNodeModuleSources =
(if pnpmWorkspaceYaml == null
then []
else [
{name = "pnpm-workspace.yaml"; value = pnpmWorkspaceYaml;}
]
) ++ extraNodeModuleSources;
# Computed values that loop over something
nodeModulesDirs =
if isWorkspace then
["node_modules"] ++ (map (c: "${c}/node_modules") components)
else ["node_modules"];
filterString = concatStringsSep " " (
["--recursive" "--stream"] ++
map (c: "--filter ./${c}") components
) + " ";
buildScripts = ''
pnpm run ${optionalString isWorkspace filterString}${script}
'';
# Flag derived from value computed above, indicating the single dist
# should be copied as $out directly, rather than $out/${distDir}
computedDistDirIsOut =
length distDirs == 1 && !isWorkspace;
in
stdenv.mkDerivation (
recursiveUpdate
(rec {
inherit src name nativeBuildInputs;

postUnpack = ''
${optionalString (pnpmWorkspaceYaml != null) ''
cp ${pnpmWorkspaceYaml} pnpm-workspace.yaml
''}
${forEachComponent (component:
''mkdir -p "${component}"'')
}
'';

configurePhase = ''
export HOME=$NIX_BUILD_TOP # Some packages need a writable HOME
export npm_config_nodedir=${nodejs}
Expand All @@ -57,12 +114,11 @@ in
${if installInPlace
then passthru.nodeModules.buildPhase
else ''
${if !copyNodeModules
then "ln -s"
else "cp -r"
} ${passthru.nodeModules}/node_modules node_modules
''
else
forEachConcat (
nodeModulesDir: ''
${copyLink} ${passthru.nodeModules}/${nodeModulesDir}${rsyncSlash} ${nodeModulesDir}
'') nodeModulesDirs
}
runHook postConfigure
Expand All @@ -77,15 +133,25 @@ in
runHook preBuild
pnpm run ${script}
${buildScripts}
runHook postBuild
'';

installPhase = ''
runHook preInstall
${if distDir == "." then "cp -r" else "mv"} ${distDir} $out
${if computedDistDirIsOut then ''
${if distDir == "." then "cp -r" else "mv"} ${distDir} $out
''
else ''
mkdir -p $out
${forEachConcat (dDir: ''
cp -r --parents ${dDir} $out
'') distDirs
}
''
}
runHook postInstall
'';
Expand Down Expand Up @@ -119,18 +185,19 @@ in
inherit nativeBuildInputs;

unpackPhase = concatStringsSep "\n"
(
( [ # components is an empty list for non workspace builds
(forEachComponent (component: ''
mkdir -p "${component}"
'')) ] ++
map
(v:
let
nv = if isAttrs v then v else { name = "."; value = v; };
in
"cp -vr ${nv.value} ${nv.name}"
"cp -vr \"${nv.value}\" \"${nv.name}\""
)
([
{ name = "package.json"; value = packageJSON; }
{ name = "pnpm-lock.yaml"; value = passthru.patchedLockfileYaml; }
] ++ extraNodeModuleSources)
([{ name = "pnpm-lock.yaml"; value = passthru.patchedLockfileYaml; }]
++ packageFilesWithoutLockfile)
);

buildPhase = ''
Expand All @@ -147,20 +214,24 @@ in
else "cp -RL"
} ${passthru.pnpmStore} $(pnpm store path)
${lib.optionalString copyPnpmStore "chmod -R +w $(pnpm store path)"}
${optionalString copyPnpmStore "chmod -R +w $(pnpm store path)"}
${concatStringsSep "\n" (
mapAttrsToList
(n: v: ''export ${n}="${v}"'')
installEnv
)}
pnpm install ${optionalString noDevDependencies "--prod "}--frozen-lockfile --offline
pnpm install --stream ${optionalString noDevDependencies "--prod "}--frozen-lockfile --offline
'';

installPhase = ''
mkdir -p $out
cp -r node_modules/. $out/node_modules
${forEachComponent (component: ''
mkdir -p $out/"${component}"
cp -r "${component}/node_modules" $out/"${component}/node_modules"
'')}
'';
};
};
Expand Down

0 comments on commit a17fd10

Please sign in to comment.