blob: d5bf407b2bb6cf17d6044e958a9755bc1a97f3df [file] [log] [blame]
Skyler Greyec5ffff2023-03-26 10:44:35 +00001# This file originates from node2nix
2
Skyler Grey1909a0b2023-05-03 20:33:53 +00003{ lib, stdenv, nodejs, python2, pkgs, libtool, runCommand, writeTextFile, writeShellScript }:
Skyler Greyec5ffff2023-03-26 10:44:35 +00004
5let
6 # Workaround to cope with utillinux in Nixpkgs 20.09 and util-linux in Nixpkgs master
7 utillinux = if pkgs ? utillinux then pkgs.utillinux else pkgs.util-linux;
8
9 python = if nodejs ? python then nodejs.python else python2;
10
11 # Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise
Skyler Grey1909a0b2023-05-03 20:33:53 +000012 tarWrapper = runCommand "tarWrapper" { } ''
Skyler Greyec5ffff2023-03-26 10:44:35 +000013 mkdir -p $out/bin
14
15 cat > $out/bin/tar <<EOF
16 #! ${stdenv.shell} -e
17 $(type -p tar) "\$@" --warning=no-unknown-keyword --delay-directory-restore
18 EOF
19
20 chmod +x $out/bin/tar
21 '';
22
23 # Function that generates a TGZ file from a NPM project
24 buildNodeSourceDist =
25 { name, version, src, ... }:
26
27 stdenv.mkDerivation {
28 name = "node-tarball-${name}-${version}";
29 inherit src;
30 buildInputs = [ nodejs ];
31 buildPhase = ''
32 export HOME=$TMPDIR
33 tgzFile=$(npm pack | tail -n 1) # Hooks to the pack command will add output (https://docs.npmjs.com/misc/scripts)
34 '';
35 installPhase = ''
36 mkdir -p $out/tarballs
37 mv $tgzFile $out/tarballs
38 mkdir -p $out/nix-support
39 echo "file source-dist $out/tarballs/$tgzFile" >> $out/nix-support/hydra-build-products
40 '';
41 };
42
43 # Common shell logic
44 installPackage = writeShellScript "install-package" ''
45 installPackage() {
46 local packageName=$1 src=$2
47
48 local strippedName
49
50 local DIR=$PWD
51 cd $TMPDIR
52
53 unpackFile $src
54
55 # Make the base dir in which the target dependency resides first
56 mkdir -p "$(dirname "$DIR/$packageName")"
57
58 if [ -f "$src" ]
59 then
60 # Figure out what directory has been unpacked
61 packageDir="$(find . -maxdepth 1 -type d | tail -1)"
62
63 # Restore write permissions to make building work
64 find "$packageDir" -type d -exec chmod u+x {} \;
65 chmod -R u+w "$packageDir"
66
67 # Move the extracted tarball into the output folder
68 mv "$packageDir" "$DIR/$packageName"
69 elif [ -d "$src" ]
70 then
71 # Get a stripped name (without hash) of the source directory.
72 # On old nixpkgs it's already set internally.
73 if [ -z "$strippedName" ]
74 then
75 strippedName="$(stripHash $src)"
76 fi
77
78 # Restore write permissions to make building work
79 chmod -R u+w "$strippedName"
80
81 # Move the extracted directory into the output folder
82 mv "$strippedName" "$DIR/$packageName"
83 fi
84
85 # Change to the package directory to install dependencies
86 cd "$DIR/$packageName"
87 }
88 '';
89
90 # Bundle the dependencies of the package
91 #
92 # Only include dependencies if they don't exist. They may also be bundled in the package.
Skyler Grey1909a0b2023-05-03 20:33:53 +000093 includeDependencies = { dependencies }:
94 lib.optionalString (dependencies != [ ]) (
Skyler Greyec5ffff2023-03-26 10:44:35 +000095 ''
96 mkdir -p node_modules
97 cd node_modules
98 ''
Skyler Grey1909a0b2023-05-03 20:33:53 +000099 + (lib.concatMapStrings
100 (dependency:
101 ''
102 if [ ! -e "${dependency.packageName}" ]; then
103 ${composePackage dependency}
104 fi
105 ''
106 )
107 dependencies)
Skyler Greyec5ffff2023-03-26 10:44:35 +0000108 + ''
109 cd ..
110 ''
111 );
112
113 # Recursively composes the dependencies of a package
Skyler Grey1909a0b2023-05-03 20:33:53 +0000114 composePackage = { name, packageName, src, dependencies ? [ ], ... }@args:
Skyler Greyec5ffff2023-03-26 10:44:35 +0000115 builtins.addErrorContext "while evaluating node package '${packageName}'" ''
116 installPackage "${packageName}" "${src}"
117 ${includeDependencies { inherit dependencies; }}
118 cd ..
119 ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
120 '';
121
Skyler Grey1909a0b2023-05-03 20:33:53 +0000122 pinpointDependencies = { dependencies, production }:
Skyler Greyec5ffff2023-03-26 10:44:35 +0000123 let
124 pinpointDependenciesFromPackageJSON = writeTextFile {
125 name = "pinpointDependencies.js";
126 text = ''
127 var fs = require('fs');
128 var path = require('path');
129
130 function resolveDependencyVersion(location, name) {
131 if(location == process.env['NIX_STORE']) {
132 return null;
133 } else {
134 var dependencyPackageJSON = path.join(location, "node_modules", name, "package.json");
135
136 if(fs.existsSync(dependencyPackageJSON)) {
137 var dependencyPackageObj = JSON.parse(fs.readFileSync(dependencyPackageJSON));
138
139 if(dependencyPackageObj.name == name) {
140 return dependencyPackageObj.version;
141 }
142 } else {
143 return resolveDependencyVersion(path.resolve(location, ".."), name);
144 }
145 }
146 }
147
148 function replaceDependencies(dependencies) {
149 if(typeof dependencies == "object" && dependencies !== null) {
150 for(var dependency in dependencies) {
151 var resolvedVersion = resolveDependencyVersion(process.cwd(), dependency);
152
153 if(resolvedVersion === null) {
154 process.stderr.write("WARNING: cannot pinpoint dependency: "+dependency+", context: "+process.cwd()+"\n");
155 } else {
156 dependencies[dependency] = resolvedVersion;
157 }
158 }
159 }
160 }
161
162 /* Read the package.json configuration */
163 var packageObj = JSON.parse(fs.readFileSync('./package.json'));
164
165 /* Pinpoint all dependencies */
166 replaceDependencies(packageObj.dependencies);
167 if(process.argv[2] == "development") {
168 replaceDependencies(packageObj.devDependencies);
169 }
170 else {
171 packageObj.devDependencies = {};
172 }
173 replaceDependencies(packageObj.optionalDependencies);
174 replaceDependencies(packageObj.peerDependencies);
175
176 /* Write the fixed package.json file */
177 fs.writeFileSync("package.json", JSON.stringify(packageObj, null, 2));
178 '';
179 };
180 in
181 ''
182 node ${pinpointDependenciesFromPackageJSON} ${if production then "production" else "development"}
183
184 ${lib.optionalString (dependencies != [])
185 ''
186 if [ -d node_modules ]
187 then
188 cd node_modules
189 ${lib.concatMapStrings (dependency: pinpointDependenciesOfPackage dependency) dependencies}
190 cd ..
191 fi
192 ''}
193 '';
194
195 # Recursively traverses all dependencies of a package and pinpoints all
196 # dependencies in the package.json file to the versions that are actually
197 # being used.
198
Skyler Grey1909a0b2023-05-03 20:33:53 +0000199 pinpointDependenciesOfPackage = { packageName, dependencies ? [ ], production ? true, ... }@args:
Skyler Greyec5ffff2023-03-26 10:44:35 +0000200 ''
201 if [ -d "${packageName}" ]
202 then
203 cd "${packageName}"
204 ${pinpointDependencies { inherit dependencies production; }}
205 cd ..
206 ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
207 fi
208 '';
209
210 # Extract the Node.js source code which is used to compile packages with
211 # native bindings
Skyler Grey1909a0b2023-05-03 20:33:53 +0000212 nodeSources = runCommand "node-sources" { } ''
Skyler Greyec5ffff2023-03-26 10:44:35 +0000213 tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
214 mv node-* $out
215 '';
216
217 # Script that adds _integrity fields to all package.json files to prevent NPM from consulting the cache (that is empty)
218 addIntegrityFieldsScript = writeTextFile {
219 name = "addintegrityfields.js";
220 text = ''
221 var fs = require('fs');
222 var path = require('path');
223
224 function augmentDependencies(baseDir, dependencies) {
225 for(var dependencyName in dependencies) {
226 var dependency = dependencies[dependencyName];
227
228 // Open package.json and augment metadata fields
229 var packageJSONDir = path.join(baseDir, "node_modules", dependencyName);
230 var packageJSONPath = path.join(packageJSONDir, "package.json");
231
232 if(fs.existsSync(packageJSONPath)) { // Only augment packages that exist. Sometimes we may have production installs in which development dependencies can be ignored
233 console.log("Adding metadata fields to: "+packageJSONPath);
234 var packageObj = JSON.parse(fs.readFileSync(packageJSONPath));
235
236 if(dependency.integrity) {
237 packageObj["_integrity"] = dependency.integrity;
238 } else {
239 packageObj["_integrity"] = "sha1-000000000000000000000000000="; // When no _integrity string has been provided (e.g. by Git dependencies), add a dummy one. It does not seem to harm and it bypasses downloads.
240 }
241
242 if(dependency.resolved) {
243 packageObj["_resolved"] = dependency.resolved; // Adopt the resolved property if one has been provided
244 } else {
245 packageObj["_resolved"] = dependency.version; // Set the resolved version to the version identifier. This prevents NPM from cloning Git repositories.
246 }
247
248 if(dependency.from !== undefined) { // Adopt from property if one has been provided
249 packageObj["_from"] = dependency.from;
250 }
251
252 fs.writeFileSync(packageJSONPath, JSON.stringify(packageObj, null, 2));
253 }
254
255 // Augment transitive dependencies
256 if(dependency.dependencies !== undefined) {
257 augmentDependencies(packageJSONDir, dependency.dependencies);
258 }
259 }
260 }
261
262 if(fs.existsSync("./package-lock.json")) {
263 var packageLock = JSON.parse(fs.readFileSync("./package-lock.json"));
264
265 if(![1, 2].includes(packageLock.lockfileVersion)) {
266 process.stderr.write("Sorry, I only understand lock file versions 1 and 2!\n");
267 process.exit(1);
268 }
269
270 if(packageLock.dependencies !== undefined) {
271 augmentDependencies(".", packageLock.dependencies);
272 }
273 }
274 '';
275 };
276
277 # Reconstructs a package-lock file from the node_modules/ folder structure and package.json files with dummy sha1 hashes
278 reconstructPackageLock = writeTextFile {
279 name = "reconstructpackagelock.js";
280 text = ''
281 var fs = require('fs');
282 var path = require('path');
283
284 var packageObj = JSON.parse(fs.readFileSync("package.json"));
285
286 var lockObj = {
287 name: packageObj.name,
288 version: packageObj.version,
289 lockfileVersion: 2,
290 requires: true,
291 packages: {
292 "": {
293 name: packageObj.name,
294 version: packageObj.version,
295 license: packageObj.license,
296 bin: packageObj.bin,
297 dependencies: packageObj.dependencies,
298 engines: packageObj.engines,
299 optionalDependencies: packageObj.optionalDependencies
300 }
301 },
302 dependencies: {}
303 };
304
305 function augmentPackageJSON(filePath, packages, dependencies) {
306 var packageJSON = path.join(filePath, "package.json");
307 if(fs.existsSync(packageJSON)) {
308 var packageObj = JSON.parse(fs.readFileSync(packageJSON));
309 packages[filePath] = {
310 version: packageObj.version,
311 integrity: "sha1-000000000000000000000000000=",
312 dependencies: packageObj.dependencies,
313 engines: packageObj.engines,
314 optionalDependencies: packageObj.optionalDependencies
315 };
316 dependencies[packageObj.name] = {
317 version: packageObj.version,
318 integrity: "sha1-000000000000000000000000000=",
319 dependencies: {}
320 };
321 processDependencies(path.join(filePath, "node_modules"), packages, dependencies[packageObj.name].dependencies);
322 }
323 }
324
325 function processDependencies(dir, packages, dependencies) {
326 if(fs.existsSync(dir)) {
327 var files = fs.readdirSync(dir);
328
329 files.forEach(function(entry) {
330 var filePath = path.join(dir, entry);
331 var stats = fs.statSync(filePath);
332
333 if(stats.isDirectory()) {
334 if(entry.substr(0, 1) == "@") {
335 // When we encounter a namespace folder, augment all packages belonging to the scope
336 var pkgFiles = fs.readdirSync(filePath);
337
338 pkgFiles.forEach(function(entry) {
339 if(stats.isDirectory()) {
340 var pkgFilePath = path.join(filePath, entry);
341 augmentPackageJSON(pkgFilePath, packages, dependencies);
342 }
343 });
344 } else {
345 augmentPackageJSON(filePath, packages, dependencies);
346 }
347 }
348 });
349 }
350 }
351
352 processDependencies("node_modules", lockObj.packages, lockObj.dependencies);
353
354 fs.writeFileSync("package-lock.json", JSON.stringify(lockObj, null, 2));
355 '';
356 };
357
358 # Script that links bins defined in package.json to the node_modules bin directory
359 # NPM does not do this for top-level packages itself anymore as of v7
360 linkBinsScript = writeTextFile {
361 name = "linkbins.js";
362 text = ''
363 var fs = require('fs');
364 var path = require('path');
365
366 var packageObj = JSON.parse(fs.readFileSync("package.json"));
367
368 var nodeModules = Array(packageObj.name.split("/").length).fill("..").join(path.sep);
369
370 if(packageObj.bin !== undefined) {
371 fs.mkdirSync(path.join(nodeModules, ".bin"))
372
373 if(typeof packageObj.bin == "object") {
374 Object.keys(packageObj.bin).forEach(function(exe) {
375 if(fs.existsSync(packageObj.bin[exe])) {
376 console.log("linking bin '" + exe + "'");
377 fs.symlinkSync(
378 path.join("..", packageObj.name, packageObj.bin[exe]),
379 path.join(nodeModules, ".bin", exe)
380 );
381 }
382 else {
383 console.log("skipping non-existent bin '" + exe + "'");
384 }
385 })
386 }
387 else {
388 if(fs.existsSync(packageObj.bin)) {
389 console.log("linking bin '" + packageObj.bin + "'");
390 fs.symlinkSync(
391 path.join("..", packageObj.name, packageObj.bin),
392 path.join(nodeModules, ".bin", packageObj.name.split("/").pop())
393 );
394 }
395 else {
396 console.log("skipping non-existent bin '" + packageObj.bin + "'");
397 }
398 }
399 }
400 else if(packageObj.directories !== undefined && packageObj.directories.bin !== undefined) {
401 fs.mkdirSync(path.join(nodeModules, ".bin"))
402
403 fs.readdirSync(packageObj.directories.bin).forEach(function(exe) {
404 if(fs.existsSync(path.join(packageObj.directories.bin, exe))) {
405 console.log("linking bin '" + exe + "'");
406 fs.symlinkSync(
407 path.join("..", packageObj.name, packageObj.directories.bin, exe),
408 path.join(nodeModules, ".bin", exe)
409 );
410 }
411 else {
412 console.log("skipping non-existent bin '" + exe + "'");
413 }
414 })
415 }
416 '';
417 };
418
Skyler Grey1909a0b2023-05-03 20:33:53 +0000419 prepareAndInvokeNPM = { packageName, bypassCache, reconstructLock, npmFlags, production }:
Skyler Greyec5ffff2023-03-26 10:44:35 +0000420 let
421 forceOfflineFlag = if bypassCache then "--offline" else "--registry http://www.example.com";
422 in
423 ''
Skyler Grey1909a0b2023-05-03 20:33:53 +0000424 # Pinpoint the versions of all dependencies to the ones that are actually being used
425 echo "pinpointing versions of dependencies..."
426 source $pinpointDependenciesScriptPath
Skyler Greyec5ffff2023-03-26 10:44:35 +0000427
Skyler Grey1909a0b2023-05-03 20:33:53 +0000428 # Patch the shebangs of the bundled modules to prevent them from
429 # calling executables outside the Nix store as much as possible
430 patchShebangs .
Skyler Greyec5ffff2023-03-26 10:44:35 +0000431
Skyler Grey1909a0b2023-05-03 20:33:53 +0000432 # Deploy the Node.js package by running npm install. Since the
433 # dependencies have been provided already by ourselves, it should not
434 # attempt to install them again, which is good, because we want to make
435 # it Nix's responsibility. If it needs to install any dependencies
436 # anyway (e.g. because the dependency parameters are
437 # incomplete/incorrect), it fails.
438 #
439 # The other responsibilities of NPM are kept -- version checks, build
440 # steps, postprocessing etc.
Skyler Greyec5ffff2023-03-26 10:44:35 +0000441
Skyler Grey1909a0b2023-05-03 20:33:53 +0000442 export HOME=$TMPDIR
443 cd "${packageName}"
444 runHook preRebuild
Skyler Greyec5ffff2023-03-26 10:44:35 +0000445
Skyler Grey1909a0b2023-05-03 20:33:53 +0000446 ${lib.optionalString bypassCache ''
447 ${lib.optionalString reconstructLock ''
448 if [ -f package-lock.json ]
449 then
450 echo "WARNING: Reconstruct lock option enabled, but a lock file already exists!"
451 echo "This will most likely result in version mismatches! We will remove the lock file and regenerate it!"
452 rm package-lock.json
453 else
454 echo "No package-lock.json file found, reconstructing..."
455 fi
Skyler Greyec5ffff2023-03-26 10:44:35 +0000456
Skyler Grey1909a0b2023-05-03 20:33:53 +0000457 node ${reconstructPackageLock}
Skyler Greyec5ffff2023-03-26 10:44:35 +0000458 ''}
459
Skyler Grey1909a0b2023-05-03 20:33:53 +0000460 node ${addIntegrityFieldsScript}
461 ''}
Skyler Greyec5ffff2023-03-26 10:44:35 +0000462
Skyler Grey1909a0b2023-05-03 20:33:53 +0000463 npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} rebuild
Skyler Greyec5ffff2023-03-26 10:44:35 +0000464
Skyler Grey1909a0b2023-05-03 20:33:53 +0000465 runHook postRebuild
Skyler Greyec5ffff2023-03-26 10:44:35 +0000466
Skyler Grey1909a0b2023-05-03 20:33:53 +0000467 if [ "''${dontNpmInstall-}" != "1" ]
468 then
469 # NPM tries to download packages even when they already exist if npm-shrinkwrap is used.
470 rm -f npm-shrinkwrap.json
Skyler Greyec5ffff2023-03-26 10:44:35 +0000471
Skyler Grey1909a0b2023-05-03 20:33:53 +0000472 npm ${forceOfflineFlag} --nodedir=${nodeSources} --no-bin-links --ignore-scripts ${npmFlags} ${lib.optionalString production "--production"} install
473 fi
474
475 # Link executables defined in package.json
476 node ${linkBinsScript}
Skyler Greyec5ffff2023-03-26 10:44:35 +0000477 '';
478
479 # Builds and composes an NPM package including all its dependencies
480 buildNodePackage =
481 { name
482 , packageName
483 , version ? null
Skyler Grey1909a0b2023-05-03 20:33:53 +0000484 , dependencies ? [ ]
485 , buildInputs ? [ ]
Skyler Greyec5ffff2023-03-26 10:44:35 +0000486 , production ? true
487 , npmFlags ? ""
488 , dontNpmInstall ? false
489 , bypassCache ? false
490 , reconstructLock ? false
491 , preRebuild ? ""
492 , dontStrip ? true
493 , unpackPhase ? "true"
494 , buildPhase ? "true"
Skyler Grey1909a0b2023-05-03 20:33:53 +0000495 , meta ? { }
496 , ...
497 }@args:
Skyler Greyec5ffff2023-03-26 10:44:35 +0000498
499 let
500 extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "preRebuild" "unpackPhase" "buildPhase" "meta" ];
501 in
502 stdenv.mkDerivation ({
503 name = "${name}${if version == null then "" else "-${version}"}";
504 buildInputs = [ tarWrapper python nodejs ]
505 ++ lib.optional (stdenv.isLinux) utillinux
506 ++ lib.optional (stdenv.isDarwin) libtool
507 ++ buildInputs;
508
509 inherit nodejs;
510
511 inherit dontStrip; # Stripping may fail a build for some package deployments
512 inherit dontNpmInstall preRebuild unpackPhase buildPhase;
513
514 compositionScript = composePackage args;
515 pinpointDependenciesScript = pinpointDependenciesOfPackage args;
516
517 passAsFile = [ "compositionScript" "pinpointDependenciesScript" ];
518
519 installPhase = ''
520 source ${installPackage}
521
522 # Create and enter a root node_modules/ folder
523 mkdir -p $out/lib/node_modules
524 cd $out/lib/node_modules
525
526 # Compose the package and all its dependencies
527 source $compositionScriptPath
528
529 ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
530
531 # Create symlink to the deployed executable folder, if applicable
532 if [ -d "$out/lib/node_modules/.bin" ]
533 then
534 ln -s $out/lib/node_modules/.bin $out/bin
535
536 # Patch the shebang lines of all the executables
537 ls $out/bin/* | while read i
538 do
539 file="$(readlink -f "$i")"
540 chmod u+rwx "$file"
541 patchShebangs "$file"
542 done
543 fi
544
545 # Create symlinks to the deployed manual page folders, if applicable
546 if [ -d "$out/lib/node_modules/${packageName}/man" ]
547 then
548 mkdir -p $out/share
549 for dir in "$out/lib/node_modules/${packageName}/man/"*
550 do
551 mkdir -p $out/share/man/$(basename "$dir")
552 for page in "$dir"/*
553 do
554 ln -s $page $out/share/man/$(basename "$dir")
555 done
556 done
557 fi
558
559 # Run post install hook, if provided
560 runHook postInstall
561 '';
562
563 meta = {
564 # default to Node.js' platforms
565 platforms = nodejs.meta.platforms;
566 } // meta;
567 } // extraArgs);
568
569 # Builds a node environment (a node_modules folder and a set of binaries)
570 buildNodeDependencies =
571 { name
572 , packageName
573 , version ? null
574 , src
Skyler Grey1909a0b2023-05-03 20:33:53 +0000575 , dependencies ? [ ]
576 , buildInputs ? [ ]
Skyler Greyec5ffff2023-03-26 10:44:35 +0000577 , production ? true
578 , npmFlags ? ""
579 , dontNpmInstall ? false
580 , bypassCache ? false
581 , reconstructLock ? false
582 , dontStrip ? true
583 , unpackPhase ? "true"
584 , buildPhase ? "true"
Skyler Grey1909a0b2023-05-03 20:33:53 +0000585 , ...
586 }@args:
Skyler Greyec5ffff2023-03-26 10:44:35 +0000587
588 let
589 extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" ];
590 in
Skyler Grey1909a0b2023-05-03 20:33:53 +0000591 stdenv.mkDerivation ({
592 name = "node-dependencies-${name}${if version == null then "" else "-${version}"}";
Skyler Greyec5ffff2023-03-26 10:44:35 +0000593
Skyler Grey1909a0b2023-05-03 20:33:53 +0000594 buildInputs = [ tarWrapper python nodejs ]
595 ++ lib.optional (stdenv.isLinux) utillinux
596 ++ lib.optional (stdenv.isDarwin) libtool
597 ++ buildInputs;
Skyler Greyec5ffff2023-03-26 10:44:35 +0000598
Skyler Grey1909a0b2023-05-03 20:33:53 +0000599 inherit dontStrip; # Stripping may fail a build for some package deployments
600 inherit dontNpmInstall unpackPhase buildPhase;
Skyler Greyec5ffff2023-03-26 10:44:35 +0000601
Skyler Grey1909a0b2023-05-03 20:33:53 +0000602 includeScript = includeDependencies { inherit dependencies; };
603 pinpointDependenciesScript = pinpointDependenciesOfPackage args;
Skyler Greyec5ffff2023-03-26 10:44:35 +0000604
Skyler Grey1909a0b2023-05-03 20:33:53 +0000605 passAsFile = [ "includeScript" "pinpointDependenciesScript" ];
Skyler Greyec5ffff2023-03-26 10:44:35 +0000606
Skyler Grey1909a0b2023-05-03 20:33:53 +0000607 installPhase = ''
608 source ${installPackage}
Skyler Greyec5ffff2023-03-26 10:44:35 +0000609
Skyler Grey1909a0b2023-05-03 20:33:53 +0000610 mkdir -p $out/${packageName}
611 cd $out/${packageName}
Skyler Greyec5ffff2023-03-26 10:44:35 +0000612
Skyler Grey1909a0b2023-05-03 20:33:53 +0000613 source $includeScriptPath
Skyler Greyec5ffff2023-03-26 10:44:35 +0000614
Skyler Grey1909a0b2023-05-03 20:33:53 +0000615 # Create fake package.json to make the npm commands work properly
616 cp ${src}/package.json .
617 chmod 644 package.json
618 ${lib.optionalString bypassCache ''
619 if [ -f ${src}/package-lock.json ]
620 then
621 cp ${src}/package-lock.json .
622 chmod 644 package-lock.json
623 fi
624 ''}
Skyler Greyec5ffff2023-03-26 10:44:35 +0000625
Skyler Grey1909a0b2023-05-03 20:33:53 +0000626 # Go to the parent folder to make sure that all packages are pinpointed
627 cd ..
628 ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
Skyler Greyec5ffff2023-03-26 10:44:35 +0000629
Skyler Grey1909a0b2023-05-03 20:33:53 +0000630 ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
Skyler Greyec5ffff2023-03-26 10:44:35 +0000631
Skyler Grey1909a0b2023-05-03 20:33:53 +0000632 # Expose the executables that were installed
633 cd ..
634 ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
Skyler Greyec5ffff2023-03-26 10:44:35 +0000635
Skyler Grey1909a0b2023-05-03 20:33:53 +0000636 mv ${packageName} lib
637 ln -s $out/lib/node_modules/.bin $out/bin
638 '';
639 } // extraArgs);
Skyler Greyec5ffff2023-03-26 10:44:35 +0000640
641 # Builds a development shell
642 buildNodeShell =
643 { name
644 , packageName
645 , version ? null
646 , src
Skyler Grey1909a0b2023-05-03 20:33:53 +0000647 , dependencies ? [ ]
648 , buildInputs ? [ ]
Skyler Greyec5ffff2023-03-26 10:44:35 +0000649 , production ? true
650 , npmFlags ? ""
651 , dontNpmInstall ? false
652 , bypassCache ? false
653 , reconstructLock ? false
654 , dontStrip ? true
655 , unpackPhase ? "true"
656 , buildPhase ? "true"
Skyler Grey1909a0b2023-05-03 20:33:53 +0000657 , ...
658 }@args:
Skyler Greyec5ffff2023-03-26 10:44:35 +0000659
660 let
661 nodeDependencies = buildNodeDependencies args;
662 extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "unpackPhase" "buildPhase" ];
663 in
664 stdenv.mkDerivation ({
665 name = "node-shell-${name}${if version == null then "" else "-${version}"}";
666
667 buildInputs = [ python nodejs ] ++ lib.optional (stdenv.isLinux) utillinux ++ buildInputs;
668 buildCommand = ''
669 mkdir -p $out/bin
670 cat > $out/bin/shell <<EOF
671 #! ${stdenv.shell} -e
672 $shellHook
673 exec ${stdenv.shell}
674 EOF
675 chmod +x $out/bin/shell
676 '';
677
678 # Provide the dependencies in a development shell through the NODE_PATH environment variable
679 inherit nodeDependencies;
Skyler Grey1909a0b2023-05-03 20:33:53 +0000680 shellHook = lib.optionalString (dependencies != [ ]) ''
Skyler Greyec5ffff2023-03-26 10:44:35 +0000681 export NODE_PATH=${nodeDependencies}/lib/node_modules
682 export PATH="${nodeDependencies}/bin:$PATH"
683 '';
684 } // extraArgs);
685in
686{
687 buildNodeSourceDist = lib.makeOverridable buildNodeSourceDist;
688 buildNodePackage = lib.makeOverridable buildNodePackage;
689 buildNodeDependencies = lib.makeOverridable buildNodeDependencies;
690 buildNodeShell = lib.makeOverridable buildNodeShell;
691}