1{
2 lib,
3 fetchurl,
4 stdenv,
5 callPackages,
6 runCommand,
7 cctools,
8}:
9
10let
11 inherit (builtins)
12 match
13 elemAt
14 toJSON
15 removeAttrs
16 ;
17 inherit (lib) importJSON mapAttrs;
18
19 matchGitHubReference = match "github(.com)?:.+";
20 getName = package: package.name or "unknown";
21 getVersion = package: package.version or "0.0.0";
22
23 # Fetch a module from package-lock.json -> packages
24 fetchModule =
25 {
26 module,
27 npmRoot ? null,
28 fetcherOpts,
29 }:
30 (
31 if module ? "resolved" && module.resolved != null then
32 (
33 let
34 # Parse scheme from URL
35 mUrl = match "(.+)://(.+)" module.resolved;
36 scheme = elemAt mUrl 0;
37 in
38 (
39 if mUrl == null then
40 (
41 assert npmRoot != null;
42 {
43 outPath = npmRoot + "/${module.resolved}";
44 }
45 )
46 else if (scheme == "http" || scheme == "https") then
47 (fetchurl (
48 {
49 url = module.resolved;
50 hash = module.integrity;
51 }
52 // fetcherOpts
53 ))
54 else if lib.hasPrefix "git" module.resolved then
55 (builtins.fetchGit (
56 {
57 url = module.resolved;
58 }
59 // fetcherOpts
60 ))
61 else
62 throw "Unsupported URL scheme: ${scheme}"
63 )
64 )
65 else
66 null
67 );
68
69 cleanModule = lib.flip removeAttrs [
70 "link" # Remove link not to symlink directories. These have been processed to store paths already.
71 "funding" # Remove funding to get rid sponsorship nag in build output
72 ];
73
74 # Manage node_modules outside of the store with hooks
75 hooks = callPackages ./hooks { };
76
77in
78lib.fix (self: {
79 importNpmLock =
80 {
81 npmRoot ? null,
82 package ? importJSON (npmRoot + "/package.json"),
83 packageLock ? importJSON (npmRoot + "/package-lock.json"),
84 pname ? getName package,
85 version ? getVersion package,
86 # A map of additional fetcher options forwarded to the fetcher used to download the package.
87 # Example: { "node_modules/axios" = { curlOptsList = [ "--verbose" ]; }; }
88 # This will download the axios package with curl's verbose option.
89 fetcherOpts ? { },
90 # A map from node_module path to an alternative package to use instead of fetching the source in package-lock.json.
91 # Example: { "node_modules/axios" = stdenv.mkDerivation { ... }; }
92 # This is useful if you want to inject custom sources for a specific package.
93 packageSourceOverrides ? { },
94 }:
95 let
96 mapLockDependencies = mapAttrs (
97 name: version:
98 (
99 # Substitute the constraint with the version of the dependency from the top-level of package-lock.
100 if
101 (
102 # if the version is `latest`
103 version == "latest"
104 ||
105 # Or if it's a github reference
106 matchGitHubReference version != null
107 )
108 then
109 packageLock'.packages.${"node_modules/${name}"}.version
110 # But not a regular version constraint
111 else
112 version
113 )
114 );
115
116 packageLock' = packageLock // {
117 packages = mapAttrs (
118 modulePath: module:
119 let
120 src =
121 packageSourceOverrides.${modulePath} or (fetchModule {
122 inherit module npmRoot;
123 fetcherOpts = fetcherOpts.${modulePath} or { };
124 });
125 in
126 cleanModule module
127 // lib.optionalAttrs (src != null) {
128 resolved = "file:${src}";
129 }
130 // lib.optionalAttrs (module ? dependencies) {
131 dependencies = mapLockDependencies module.dependencies;
132 }
133 // lib.optionalAttrs (module ? optionalDependencies) {
134 optionalDependencies = mapLockDependencies module.optionalDependencies;
135 }
136 ) packageLock.packages;
137 };
138
139 mapPackageDependencies = mapAttrs (
140 name: _: packageLock'.packages.${"node_modules/${name}"}.resolved
141 );
142
143 # Substitute dependency references in package.json with Nix store paths
144 packageJSON' =
145 package
146 // lib.optionalAttrs (package ? dependencies) {
147 dependencies = mapPackageDependencies package.dependencies;
148 }
149 // lib.optionalAttrs (package ? devDependencies) {
150 devDependencies = mapPackageDependencies package.devDependencies;
151 };
152
153 pname = package.name or "unknown";
154
155 in
156 runCommand "${pname}-${version}-sources"
157 {
158 inherit pname version;
159
160 passAsFile = [
161 "package"
162 "packageLock"
163 ];
164
165 package = toJSON packageJSON';
166 packageLock = toJSON packageLock';
167 }
168 ''
169 mkdir $out
170 cp "$packagePath" $out/package.json
171 cp "$packageLockPath" $out/package-lock.json
172 '';
173
174 # Build node modules from package.json & package-lock.json
175 buildNodeModules =
176 {
177 npmRoot ? null,
178 package ? importJSON (npmRoot + "/package.json"),
179 packageLock ? importJSON (npmRoot + "/package-lock.json"),
180 nodejs,
181 derivationArgs ? { },
182 }:
183 stdenv.mkDerivation (
184 {
185 pname = derivationArgs.pname or "${getName package}-node-modules";
186 version = derivationArgs.version or getVersion package;
187
188 dontUnpack = true;
189
190 npmDeps = self.importNpmLock {
191 inherit npmRoot package packageLock;
192 };
193
194 package = toJSON package;
195 packageLock = toJSON packageLock;
196
197 installPhase = ''
198 runHook preInstall
199 mkdir $out
200 cp package.json $out/
201 cp package-lock.json $out/
202 [[ -d node_modules ]] && mv node_modules $out/
203 runHook postInstall
204 '';
205 }
206 // derivationArgs
207 // {
208 nativeBuildInputs = [
209 nodejs
210 nodejs.passthru.python
211 hooks.npmConfigHook
212 ]
213 ++ lib.optionals stdenv.hostPlatform.isDarwin [ cctools ]
214 ++ derivationArgs.nativeBuildInputs or [ ];
215
216 passAsFile = [
217 "package"
218 "packageLock"
219 ]
220 ++ derivationArgs.passAsFile or [ ];
221
222 postPatch = ''
223 cp --no-preserve=mode "$packagePath" package.json
224 cp --no-preserve=mode "$packageLockPath" package-lock.json
225 ''
226 + derivationArgs.postPatch or "";
227 }
228 );
229
230 inherit hooks;
231 inherit (hooks) npmConfigHook linkNodeModulesHook;
232
233 __functor = self: self.importNpmLock;
234})