1{ stdenv, runCommand, nodejs, neededNatives}:
2
3{
4 name, version ? "", src,
5
6 # by default name of nodejs interpreter e.g. "nodejs-${name}"
7 namePrefix ? nodejs.interpreterName + "-",
8
9 # Node package name
10 pkgName ?
11 if version != "" then stdenv.lib.removeSuffix "-${version}" name else
12 (builtins.parseDrvName name).name,
13
14 # List or attribute set of dependencies
15 deps ? {},
16
17 # List or attribute set of peer depencies
18 peerDependencies ? {},
19
20 # List or attribute set of optional dependencies
21 optionalDependencies ? {},
22
23 # List of optional dependencies to skip
24 skipOptionalDependencies ? [],
25
26 # Whether package is binary or library
27 bin ? false,
28
29 # Additional flags passed to npm install
30 flags ? "",
31
32 # Command to be run before shell hook
33 preShellHook ? "",
34
35 # Command to be run after shell hook
36 postShellHook ? "",
37
38 # Same as https://docs.npmjs.com/files/package.json#os
39 os ? [],
40
41 # Same as https://docs.npmjs.com/files/package.json#cpu
42 cpu ? [],
43
44 # Attribute set of already resolved deps (internal),
45 # for avoiding infinite recursion
46 resolvedDeps ? {},
47
48 ...
49} @ args:
50
51with stdenv.lib;
52
53let
54 self = let
55 sources = runCommand "node-sources" {} ''
56 tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
57 mv $(find . -type d -mindepth 1 -maxdepth 1) $out
58 '';
59
60 platforms = if os == [] then nodejs.meta.platforms else
61 fold (entry: platforms:
62 let
63 filterPlatforms =
64 stdenv.lib.platforms.${removePrefix "!" entry} or [];
65 in
66 # Ignore unknown platforms
67 if filterPlatforms == [] then (if platforms == [] then nodejs.meta.platforms else platforms)
68 else
69 if hasPrefix "!" entry then
70 subtractLists (intersectLists filterPlatforms nodejs.meta.platforms) platforms
71 else
72 platforms ++ (intersectLists filterPlatforms nodejs.meta.platforms)
73 ) [] os;
74
75 mapDependencies = deps: f: rec {
76 # Convert deps to attribute set
77 attrDeps = if isAttrs deps then deps else
78 (listToAttrs (map (dep: nameValuePair dep.name dep) deps));
79
80 # All required node modules, without already resolved dependencies
81 # Also override with already resolved dependencies
82 requiredDeps = mapAttrs (name: dep:
83 dep.override {
84 resolvedDeps = resolvedDeps // { "${name}" = self; };
85 }
86 ) (filterAttrs f (removeAttrs attrDeps (attrNames resolvedDeps)));
87
88 # Recursive dependencies that we want to avoid with shim creation
89 recursiveDeps = filterAttrs f (removeAttrs attrDeps (attrNames requiredDeps));
90 };
91
92 _dependencies = mapDependencies deps (name: dep:
93 dep.pkgName != pkgName);
94 _optionalDependencies = mapDependencies optionalDependencies (name: dep:
95 (builtins.tryEval dep).success &&
96 !(elem dep.pkgName skipOptionalDependencies)
97 );
98 _peerDependencies = mapDependencies peerDependencies (name: dep:
99 dep.pkgName != pkgName);
100
101 requiredDependencies =
102 _dependencies.requiredDeps //
103 _optionalDependencies.requiredDeps //
104 _peerDependencies.requiredDeps;
105
106 recursiveDependencies =
107 _dependencies.recursiveDeps //
108 _optionalDependencies.recursiveDeps //
109 _peerDependencies.recursiveDeps;
110
111 patchShebangs = dir: ''
112 node=`type -p node`
113 coffee=`type -p coffee || true`
114 find -L ${dir} -type f -print0 | xargs -0 grep -Il . | \
115 xargs sed --follow-symlinks -i \
116 -e 's@#!/usr/bin/env node@#!'"$node"'@' \
117 -e 's@#!/usr/bin/env coffee@#!'"$coffee"'@' \
118 -e 's@#!/.*/node@#!'"$node"'@' \
119 -e 's@#!/.*/coffee@#!'"$coffee"'@' || true
120 '';
121
122 in stdenv.mkDerivation ({
123 inherit src;
124
125 configurePhase = ''
126 runHook preConfigure
127
128 ${patchShebangs "./"}
129
130 # Some version specifiers (latest, unstable, URLs, file paths) force NPM
131 # to make remote connections or consult paths outside the Nix store.
132 # The following JavaScript replaces these by * to prevent that:
133 # Also some packages require a specific npm version because npm may
134 # resovle dependencies differently, but npm is not used by Nix for dependency
135 # reslution, so these requirements are dropped.
136
137 (
138 cat <<EOF
139 var fs = require('fs');
140 var url = require('url');
141
142 /*
143 * Replaces an impure version specification by *
144 */
145 function replaceImpureVersionSpec(versionSpec) {
146 var parsedUrl = url.parse(versionSpec);
147
148 if(versionSpec == "latest" || versionSpec == "unstable" ||
149 versionSpec.substr(0, 2) == ".." || dependency.substr(0, 2) == "./" || dependency.substr(0, 2) == "~/" || dependency.substr(0, 1) == '/' || /^[^/]+\/[^/]+$/.test(versionSpec))
150 return '*';
151 else if(parsedUrl.protocol == "git:" || parsedUrl.protocol == "git+ssh:" || parsedUrl.protocol == "git+http:" || parsedUrl.protocol == "git+https:" ||
152 parsedUrl.protocol == "http:" || parsedUrl.protocol == "https:")
153 return '*';
154 else
155 return versionSpec;
156 }
157
158 var packageObj = JSON.parse(fs.readFileSync('./package.json'));
159
160 /* Replace dependencies */
161 if(packageObj.dependencies !== undefined) {
162 for(var dependency in packageObj.dependencies) {
163 var versionSpec = packageObj.dependencies[dependency];
164 packageObj.dependencies[dependency] = replaceImpureVersionSpec(versionSpec);
165 }
166 }
167
168 /* Replace development dependencies */
169 if(packageObj.devDependencies !== undefined) {
170 for(var dependency in packageObj.devDependencies) {
171 var versionSpec = packageObj.devDependencies[dependency];
172 packageObj.devDependencies[dependency] = replaceImpureVersionSpec(versionSpec);
173 }
174 }
175
176 /* Replace optional dependencies */
177 if(packageObj.optionalDependencies !== undefined) {
178 for(var dependency in packageObj.optionalDependencies) {
179 var versionSpec = packageObj.optionalDependencies[dependency];
180 packageObj.optionalDependencies[dependency] = replaceImpureVersionSpec(versionSpec);
181 }
182 }
183
184 /* Ignore npm version requirement */
185 if(packageObj.engines) {
186 delete packageObj.engines.npm;
187 }
188
189 /* Write the fixed JSON file */
190 fs.writeFileSync("package.json", JSON.stringify(packageObj));
191 EOF
192 ) | node
193
194 # We do not handle shrinkwraps yet
195 rm npm-shrinkwrap.json 2>/dev/null || true
196
197 mkdir ../build-dir
198 (
199 cd ../build-dir
200 mkdir node_modules
201
202 # Symlink or copy dependencies for node modules
203 # copy is needed if dependency has recursive dependencies,
204 # because node can't follow symlinks while resolving recursive deps.
205 ${concatMapStrings (dep:
206 if dep.recursiveDeps == [] then ''
207 ln -sv ${dep}/lib/node_modules/${dep.pkgName} node_modules/
208 '' else ''
209 cp -R ${dep}/lib/node_modules/${dep.pkgName} node_modules/
210 ''
211 ) (attrValues requiredDependencies)}
212
213 # Create shims for recursive dependenceies
214 ${concatMapStrings (dep: ''
215 mkdir -p node_modules/${dep.pkgName}
216 cat > node_modules/${dep.pkgName}/package.json <<EOF
217 {
218 "name": "${dep.pkgName}",
219 "version": "${getVersion dep}"
220 }
221 EOF
222 '') (attrValues recursiveDependencies)}
223 )
224
225 export HOME=$PWD/../build-dir
226 runHook postConfigure
227 '';
228
229 buildPhase = ''
230 runHook preBuild
231
232 # If source was a file, repackage it, so npm pre/post publish hooks are not triggered,
233 if [[ -f $src ]]; then
234 GZIP=-1 tar -czf ../build-dir/package.tgz ./
235 export src=$HOME/package.tgz
236 else
237 export src=$PWD
238 fi
239
240 # Install package
241 (cd $HOME && npm --registry http://www.example.com --nodedir=${sources} install $src --fetch-retries 0 ${flags})
242
243 runHook postBuild
244 '';
245
246 installPhase = ''
247 runHook preInstall
248
249 (
250 cd $HOME
251
252 # Remove shims
253 ${concatMapStrings (dep: ''
254 rm node_modules/${dep.pkgName}/package.json
255 rmdir node_modules/${dep.pkgName}
256 '') (attrValues recursiveDependencies)}
257
258 mkdir -p $out/lib/node_modules
259
260 # Install manual
261 mv node_modules/${pkgName} $out/lib/node_modules
262 rm -fR $out/lib/node_modules/${pkgName}/node_modules
263 cp -r node_modules $out/lib/node_modules/${pkgName}/node_modules
264
265 if [ -e "$out/lib/node_modules/${pkgName}/man" ]; then
266 mkdir -p $out/share
267 for dir in "$out/lib/node_modules/${pkgName}/man/"*; do
268 mkdir -p $out/share/man/$(basename "$dir")
269 for page in "$dir"/*; do
270 ln -sv $page $out/share/man/$(basename "$dir")
271 done
272 done
273 fi
274
275 # Move peer dependencies to node_modules
276 ${concatMapStrings (dep: ''
277 mv node_modules/${dep.pkgName} $out/lib/node_modules
278 '') (attrValues _peerDependencies.requiredDeps)}
279
280 # Install binaries and patch shebangs
281 mv node_modules/.bin $out/lib/node_modules 2>/dev/null || true
282 if [ -d "$out/lib/node_modules/.bin" ]; then
283 ln -sv $out/lib/node_modules/.bin $out/bin
284 ${patchShebangs "$out/lib/node_modules/.bin/*"}
285 fi
286 )
287
288 runHook postInstall
289 '';
290
291 preFixup = ''
292 find $out -type f -print0 | xargs -0 sed -i 's|${src}|${src.name}|g'
293 '';
294
295 shellHook = ''
296 ${preShellHook}
297 export PATH=${nodejs}/bin:$(pwd)/node_modules/.bin:$PATH
298 mkdir -p node_modules
299 ${concatMapStrings (dep: ''
300 ln -sfv ${dep}/lib/node_modules/${dep.pkgName} node_modules/
301 '') (attrValues requiredDependencies)}
302 ${postShellHook}
303 '';
304
305 # Stipping does not make a lot of sense in node packages
306 dontStrip = true;
307
308 meta = {
309 inherit platforms;
310 maintainers = [ stdenv.lib.maintainers.offline ];
311 };
312
313 passthru.pkgName = pkgName;
314 } // (filterAttrs (n: v: all (k: n != k) ["deps" "resolvedDeps" "optionalDependencies"]) args) // {
315 name = namePrefix + name;
316
317 # Run the node setup hook when this package is a build input
318 propagatedNativeBuildInputs = (args.propagatedNativeBuildInputs or []) ++ [ nodejs ];
319
320 nativeBuildInputs =
321 (args.nativeBuildInputs or []) ++ neededNatives ++
322 (attrValues requiredDependencies);
323
324 # Expose list of recursive dependencies upstream, up to the package that
325 # caused recursive dependency
326 recursiveDeps =
327 (flatten (
328 map (dep: remove name dep.recursiveDeps) (attrValues requiredDependencies)
329 )) ++
330 (attrNames recursiveDependencies);
331 });
332
333in self