From bdc1af868792d535eb3e793a413728dcaa93e363 Mon Sep 17 00:00:00 2001 From: Lily Foster Date: Tue, 13 Sep 2022 14:32:14 -0400 Subject: [PATCH] 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 #236, #293, #294 [1]: https://github.com/npm/cli/commit/e46400c9484f5c66a0ba405eeb8c1340594dbf05#diff-24c01909dabbe2fc000fb5b43d14b511fb335b2f0c2e8e7a671f7d567a33d577R17-R18 [2]: https://github.com/npm/cli/issues/4308 --- nix/node-env.nix | 82 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/nix/node-env.nix b/nix/node-env.nix index 2590dd2..000a929 100644 --- a/nix/node-env.nix +++ b/nix/node-env.nix @@ -165,6 +165,9 @@ let if(process.argv[2] == "development") { replaceDependencies(packageObj.devDependencies); } + else { + delete packageObj.devDependencies; + } replaceDependencies(packageObj.optionalDependencies); /* Write the fixed package.json file */ @@ -280,25 +283,44 @@ let var lockObj = { name: packageObj.name, version: packageObj.version, - lockfileVersion: 1, + lockfileVersion: 2, requires: true, + packages: { + "": { + name: packageObj.name, + version: packageObj.version, + license: packageObj.license, + dependencies: packageObj.dependencies, + bin: packageObj.bin, + devDependencies: packageObj.devDependencies, + engines: packageObj.engines, + optionalDependencies: packageObj.optionalDependencies + } + }, dependencies: {} }; - function augmentPackageJSON(filePath, dependencies) { + function augmentPackageJSON(filePath, packages, dependencies) { var packageJSON = path.join(filePath, "package.json"); if(fs.existsSync(packageJSON)) { var packageObj = JSON.parse(fs.readFileSync(packageJSON)); + packages[filePath] = { + version: packageObj.version, + integrity: "sha1-000000000000000000000000000=", + dependencies: packageObj.dependencies, + engines: packageObj.engines, + optionalDependencies: packageObj.optionalDependencies + }; dependencies[packageObj.name] = { version: packageObj.version, integrity: "sha1-000000000000000000000000000=", dependencies: {} }; - processDependencies(path.join(filePath, "node_modules"), dependencies[packageObj.name].dependencies); + processDependencies(path.join(filePath, "node_modules"), packages, dependencies[packageObj.name].dependencies); } } - function processDependencies(dir, dependencies) { + function processDependencies(dir, packages, dependencies) { if(fs.existsSync(dir)) { var files = fs.readdirSync(dir); @@ -314,23 +336,64 @@ let pkgFiles.forEach(function(entry) { if(stats.isDirectory()) { var pkgFilePath = path.join(filePath, entry); - augmentPackageJSON(pkgFilePath, dependencies); + augmentPackageJSON(pkgFilePath, packages, dependencies); } }); } else { - augmentPackageJSON(filePath, dependencies); + augmentPackageJSON(filePath, packages, dependencies); } } }); } } - processDependencies("node_modules", lockObj.dependencies); + processDependencies("node_modules", lockObj.packages, lockObj.dependencies); fs.writeFileSync("package-lock.json", JSON.stringify(lockObj, null, 2)); ''; }; + # Script that links bins defined in package.json to the node_modules bin directory + # NPM does not do this for top-level packages itself anymore as of v7 + linkBinsScript = writeTextFile { + name = "linkbins.js"; + text = '' + var fs = require('fs'); + var path = require('path'); + + var packageObj = JSON.parse(fs.readFileSync("package.json")); + + if(packageObj.bin !== undefined) { + fs.mkdirSync(path.join("..", ".bin")) + + if(typeof packageObj.bin == "object") { + Object.keys(packageObj.bin).forEach(function(exe) { + fs.symlinkSync( + path.join("..", packageObj.name, packageObj.bin[exe]), + path.join("..", ".bin", exe) + ); + }) + } + else { + fs.symlinkSync( + path.join("..", packageObj.name, packageObj.bin), + path.join("..", ".bin", packageObj.name) + ); + } + } + else if(packageObj.directories !== undefined && packageObj.directories.bin !== undefined) { + fs.mkdirSync(path.join("..", ".bin")) + + fs.readdirSync(packageObj.directories.bin).forEach(function(exe) { + fs.symlinkSync( + path.join("..", packageObj.name, packageObj.bin[exe]), + path.join("..", ".bin", exe) + ); + }) + } + ''; + }; + prepareAndInvokeNPM = {packageName, bypassCache, reconstructLock, npmFlags, production}: let forceOfflineFlag = if bypassCache then "--offline" else "--registry http://www.example.com"; @@ -382,8 +445,11 @@ let # NPM tries to download packages even when they already exist if npm-shrinkwrap is used. rm -f npm-shrinkwrap.json - npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} install + npm ${forceOfflineFlag} --nodedir=${nodeSources} --no-bin-links ${npmFlags} ${lib.optionalString production "--production"} install fi + + # Link executables defined in package.json + node ${linkBinsScript} ''; # Builds and composes an NPM package including all its dependencies