Skip to content

Commit 49a40c2

Browse files
Emit lockfile v2 and fix bin links with NPM v7+
Lockfile v2 mostly just has a bit of extra metadata and all dependencies are hoisted to the top-level with path-specific keys in a new lock value called "packages". This update emits enough of the format that NPM v7+ seem to be happy enough with it and does not try to rewrite it and cause ENOTCACHED errors with the sandbox. As of NPM v7+, it no longer links bins for the top-level project automatically unless a global install is selected[1][2]. Given a global install would cause more problems than it would solve, I added a simple script to perform the linking ourselves and instructed `npm install` to never link them for consistency. Closes svanderburg#236, svanderburg#293, svanderburg#294 [1]: npm/cli@e46400c#diff-24c01909dabbe2fc000fb5b43d14b511fb335b2f0c2e8e7a671f7d567a33d577R17-R18 [2]: npm/cli#4308
1 parent 0263600 commit 49a40c2

File tree

3 files changed

+102
-14
lines changed

3 files changed

+102
-14
lines changed

bin/node2nix.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ var switches = [
2828
['-18', '--nodejs-18', 'Provides all settings to generate expression for usage with Node.js 18.x (default is: nodejs-14_x)'],
2929
['--supplement-input FILE', 'A supplement package JSON file that are passed as build inputs to all packages defined in the input JSON file'],
3030
['--supplement-output FILE', 'Path to a Nix expression representing a supplementing set of Nix packages provided as inputs to a project (defaults to: supplement.nix)'],
31-
['--include-peer-dependencies', 'Specifies whether to include peer dependencies. In npm 2.x, this is the default. (false by default)'],
31+
['--include-peer-dependencies', 'Specifies whether to include peer dependencies. In npm 2.x, this is the default. (true by default for Node.js 16+)'],
3232
['--no-flatten', 'Simulate pre-npm 3.x isolated dependency structure. (false by default)'],
3333
['--pkg-name NAME', 'Specifies the name of the Node.js package to use from Nixpkgs (defaults to: nodejs)'],
3434
['--registry URL', 'URL referring to the NPM packages registry. It defaults to the official NPM one, but can be overridden to support private registries'],
@@ -47,7 +47,7 @@ var parser = new optparse.OptionParser(switches);
4747
var help = false;
4848
var version = false;
4949
var production = true;
50-
var includePeerDependencies = false;
50+
var includePeerDependencies = true;
5151
var flatten = true;
5252
var inputJSON = "package.json";
5353
var outputNix = "node-packages.nix";
@@ -118,61 +118,71 @@ parser.on('development', function(arg, value) {
118118
parser.on('nodejs-4', function(arg, value) {
119119
flatten = false;
120120
nodePackage = "nodejs-4_x";
121-
byPassCache = false;
121+
bypassCache = false;
122+
includePeerDependencies = false;
122123
});
123124

124125
parser.on('nodejs-6', function(arg, value) {
125126
flatten = true;
126127
nodePackage = "nodejs-6_x";
127-
byPassCache = false;
128+
bypassCache = false;
129+
includePeerDependencies = false;
128130
});
129131

130132
parser.on('nodejs-8', function(arg, value) {
131133
flatten = true;
132134
nodePackage = "nodejs-8_x";
133135
bypassCache = true;
136+
includePeerDependencies = false;
134137
});
135138

136139
parser.on('nodejs-10', function(arg, value) {
137140
flatten = true;
138141
nodePackage = "nodejs-10_x";
139142
bypassCache = true;
143+
includePeerDependencies = false;
140144
});
141145

142146
parser.on('nodejs-12', function(arg, value) {
143147
flatten = true;
144148
nodePackage = "nodejs-12_x";
145149
bypassCache = true;
150+
includePeerDependencies = false;
146151
});
147152

148153
parser.on('nodejs-13', function(arg, value) {
149154
flatten = true;
150155
nodePackage = "nodejs-13_x";
151156
bypassCache = true;
157+
includePeerDependencies = false;
152158
});
153159

154160
parser.on('nodejs-14', function(arg, value) {
155161
flatten = true;
156162
nodePackage = "nodejs-14_x";
157163
bypassCache = true;
164+
includePeerDependencies = false;
158165
});
159166

160167
parser.on('nodejs-16', function(arg, value) {
161168
flatten = true;
162169
nodePackage = "nodejs-16_x";
163170
bypassCache = true;
171+
includePeerDependencies = true;
164172
});
165173

166174
parser.on('nodejs-17', function(arg, value) {
167175
flatten = true;
168176
nodePackage = "nodejs-17_x";
169177
bypassCache = true;
178+
includePeerDependencies = true;
170179
});
171180

172181
parser.on('nodejs-18', function(arg, value) {
173182
flatten = true;
174183
nodePackage = "nodejs-18_x";
175184
bypassCache = true;
185+
includePeerDependencies = true;
176186
});
177187

178188
parser.on('include-peer-dependencies', function(arg, value) {

lib/Package.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,16 @@ Package.prototype.resolveDependenciesAndSources = function(callback) {
196196

197197
function(callback) {
198198
if(self.deploymentConfig.includePeerDependencies) {
199-
/* Bundle the peer dependencies, if applicable */
200-
self.bundleDependencies(resolvedDependencies, self.source.config.peerDependencies, callback);
199+
/* Ignore optional peer dependencies */
200+
var peerDependencies = {};
201+
Object.keys(self.source.config.peerDependenciesMeta || {}).forEach(function(dependencyName) {
202+
/* Ignore if meta available and indicates package is optional */
203+
if (self.source.config.peerDependencies[dependencyName] && self.source.config.peerDependenciesMeta[dependencyName].optional)
204+
delete self.source.config.peerDependencies[dependencyName];
205+
});
206+
207+
/* Bundle the required peer dependencies, if applicable */
208+
self.bundleDependencies(resolvedDependencies, peerDependencies, callback);
201209
} else {
202210
callback();
203211
}

nix/node-env.nix

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,11 @@ let
165165
if(process.argv[2] == "development") {
166166
replaceDependencies(packageObj.devDependencies);
167167
}
168+
else {
169+
delete packageObj.devDependencies;
170+
}
168171
replaceDependencies(packageObj.optionalDependencies);
172+
replaceDependencies(packageObj.peerDependencies);
169173
170174
/* Write the fixed package.json file */
171175
fs.writeFileSync("package.json", JSON.stringify(packageObj, null, 2));
@@ -280,25 +284,43 @@ let
280284
var lockObj = {
281285
name: packageObj.name,
282286
version: packageObj.version,
283-
lockfileVersion: 1,
287+
lockfileVersion: 2,
284288
requires: true,
289+
packages: {
290+
"": {
291+
name: packageObj.name,
292+
version: packageObj.version,
293+
license: packageObj.license,
294+
bin: packageObj.bin,
295+
dependencies: packageObj.dependencies,
296+
engines: packageObj.engines,
297+
optionalDependencies: packageObj.optionalDependencies
298+
}
299+
},
285300
dependencies: {}
286301
};
287302
288-
function augmentPackageJSON(filePath, dependencies) {
303+
function augmentPackageJSON(filePath, packages, dependencies) {
289304
var packageJSON = path.join(filePath, "package.json");
290305
if(fs.existsSync(packageJSON)) {
291306
var packageObj = JSON.parse(fs.readFileSync(packageJSON));
307+
packages[filePath] = {
308+
version: packageObj.version,
309+
integrity: "sha1-000000000000000000000000000=",
310+
dependencies: packageObj.dependencies,
311+
engines: packageObj.engines,
312+
optionalDependencies: packageObj.optionalDependencies
313+
};
292314
dependencies[packageObj.name] = {
293315
version: packageObj.version,
294316
integrity: "sha1-000000000000000000000000000=",
295317
dependencies: {}
296318
};
297-
processDependencies(path.join(filePath, "node_modules"), dependencies[packageObj.name].dependencies);
319+
processDependencies(path.join(filePath, "node_modules"), packages, dependencies[packageObj.name].dependencies);
298320
}
299321
}
300322
301-
function processDependencies(dir, dependencies) {
323+
function processDependencies(dir, packages, dependencies) {
302324
if(fs.existsSync(dir)) {
303325
var files = fs.readdirSync(dir);
304326
@@ -314,23 +336,66 @@ let
314336
pkgFiles.forEach(function(entry) {
315337
if(stats.isDirectory()) {
316338
var pkgFilePath = path.join(filePath, entry);
317-
augmentPackageJSON(pkgFilePath, dependencies);
339+
augmentPackageJSON(pkgFilePath, packages, dependencies);
318340
}
319341
});
320342
} else {
321-
augmentPackageJSON(filePath, dependencies);
343+
augmentPackageJSON(filePath, packages, dependencies);
322344
}
323345
}
324346
});
325347
}
326348
}
327349
328-
processDependencies("node_modules", lockObj.dependencies);
350+
processDependencies("node_modules", lockObj.packages, lockObj.dependencies);
329351
330352
fs.writeFileSync("package-lock.json", JSON.stringify(lockObj, null, 2));
331353
'';
332354
};
333355

356+
# Script that links bins defined in package.json to the node_modules bin directory
357+
# NPM does not do this for top-level packages itself anymore as of v7
358+
linkBinsScript = writeTextFile {
359+
name = "linkbins.js";
360+
text = ''
361+
var fs = require('fs');
362+
var path = require('path');
363+
364+
var packageObj = JSON.parse(fs.readFileSync("package.json"));
365+
366+
var nodeModules = Array(packageObj.name.split("/").length).fill("..").join(path.sep);
367+
368+
if(packageObj.bin !== undefined) {
369+
fs.mkdirSync(path.join(nodeModules, ".bin"))
370+
371+
if(typeof packageObj.bin == "object") {
372+
Object.keys(packageObj.bin).forEach(function(exe) {
373+
fs.symlinkSync(
374+
path.join("..", packageObj.name, packageObj.bin[exe]),
375+
path.join(nodeModules, ".bin", exe)
376+
);
377+
})
378+
}
379+
else {
380+
fs.symlinkSync(
381+
path.join("..", packageObj.name, packageObj.bin),
382+
path.join(nodeModules, ".bin", packageObj.name)
383+
);
384+
}
385+
}
386+
else if(packageObj.directories !== undefined && packageObj.directories.bin !== undefined) {
387+
fs.mkdirSync(path.join(nodeModules, ".bin"))
388+
389+
fs.readdirSync(packageObj.directories.bin).forEach(function(exe) {
390+
fs.symlinkSync(
391+
path.join("..", packageObj.name, packageObj.bin[exe]),
392+
path.join(nodeModules, ".bin", exe)
393+
);
394+
})
395+
}
396+
'';
397+
};
398+
334399
prepareAndInvokeNPM = {packageName, bypassCache, reconstructLock, npmFlags, production}:
335400
let
336401
forceOfflineFlag = if bypassCache then "--offline" else "--registry http://www.example.com";
@@ -377,13 +442,18 @@ let
377442
378443
npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} rebuild
379444
445+
runHook postRebuild
446+
380447
if [ "''${dontNpmInstall-}" != "1" ]
381448
then
382449
# NPM tries to download packages even when they already exist if npm-shrinkwrap is used.
383450
rm -f npm-shrinkwrap.json
384451
385-
npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} install
452+
npm ${forceOfflineFlag} --nodedir=${nodeSources} --no-bin-links --ignore-scripts ${npmFlags} ${lib.optionalString production "--production"} install
386453
fi
454+
455+
# Link executables defined in package.json
456+
node ${linkBinsScript}
387457
'';
388458

389459
# Builds and composes an NPM package including all its dependencies

0 commit comments

Comments
 (0)