From bd576aee88860901e8e44d4fea451962bd4eea70 Mon Sep 17 00:00:00 2001 From: Ruy Adorno Date: Tue, 27 Apr 2021 15:33:52 -0400 Subject: [PATCH] feat: add walk up dir lookup to satisfy local bins Currently @npmcli/run-script already supports this walk up lookup logic that allows any nested folder to use a bin that is located inside a node_modules/.bin folder higher in the directory tree. This commit adds the same logic from: https://github.com/npm/run-script/blob/47a4d539fb07220e7215cc0e482683b76407ef9b/lib/set-path.js#L24-L28 to libnpmexec so that users may use a binary from a dependency of a nested workspace in the context of that workspaces' folder. Fixes: https://github.com/npm/cli/issues/2826 --- README.md | 2 +- lib/file-exists.js | 26 ++++++++++++++++ lib/index.js | 16 +++++----- test/index.js | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 lib/file-exists.js diff --git a/README.md b/README.md index a436c9a..cd77b99 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ await libexec({ - `call`: An alternative command to run when using `packages` option **String**, defaults to empty string. - `cache`: The path location to where the npm cache folder is placed **String** - `color`: Output should use color? **Boolean**, defaults to `false` - - `localBin`: Location to the `node_modules/.bin` folder of the local project **String**, defaults to empty string. + - `localBin`: Location to the `node_modules/.bin` folder of the local project **String** to start scanning for bin files, defaults to `./node_modules/.bin`. **libexec** will walk up the directory structure looking for `node_modules/.bin` folders in parent folders that might satisfy the current `arg` and will use that bin if found. - `locationMsg`: Overrides "at location" message when entering interactive mode **String** - `log`: Sets an optional logger **Object**, defaults to `proc-log` module usage. - `globalBin`: Location to the global space bin folder, same as: `$(npm bin -g)` **String**, defaults to empty string. diff --git a/lib/file-exists.js b/lib/file-exists.js new file mode 100644 index 0000000..1efd118 --- /dev/null +++ b/lib/file-exists.js @@ -0,0 +1,26 @@ +const { dirname, resolve } = require('path') +const { promisify } = require('util') +const stat = promisify(require('fs').stat) + +const fileExists = (file) => stat(file) + .then((stat) => stat.isFile()) + .catch(() => false) + +const localFileExists = async (dir, binName) => { + const binDir = resolve(dir, 'node_modules', '.bin') + + // return localBin if existing file is found + if (await fileExists(resolve(binDir, binName))) + return binDir + + // no more dirs left to walk up, file just does not exist + if (dir === dirname(dir)) + return false + + return localFileExists(dirname(dir), binName) +} + +module.exports = { + fileExists, + localFileExists, +} diff --git a/lib/index.js b/lib/index.js index 906a0b5..0bab753 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,6 @@ -const { delimiter, resolve } = require('path') +const { delimiter, dirname, resolve } = require('path') const { promisify } = require('util') const read = promisify(require('read')) -const stat = promisify(require('fs').stat) const Arborist = require('@npmcli/arborist') const ciDetect = require('@npmcli/ci-detect') @@ -12,15 +11,12 @@ const pacote = require('pacote') const readPackageJson = require('read-package-json-fast') const cacheInstallDir = require('./cache-install-dir.js') +const { fileExists, localFileExists } = require('./file-exists.js') const getBinFromManifest = require('./get-bin-from-manifest.js') const manifestMissing = require('./manifest-missing.js') const noTTY = require('./no-tty.js') const runScript = require('./run-script.js') -const fileExists = (file) => stat(file) - .then((stat) => stat.isFile()) - .catch(() => false) - /* istanbul ignore next */ const PATH = ( process.env.PATH || process.env.Path || process.env.path @@ -31,7 +27,7 @@ const exec = async (opts) => { args = [], call = '', color = false, - localBin = '', + localBin = resolve('./node_modules/.bin'), locationMsg = undefined, globalBin = '', output, @@ -72,8 +68,10 @@ const exec = async (opts) => { // the behavior of treating the single argument as a package name if (needPackageCommandSwap) { let binExists = false - if (await fileExists(`${localBin}/${args[0]}`)) { - pathArr.unshift(localBin) + const dir = dirname(dirname(localBin)) + const localBinPath = await localFileExists(dir, args[0]) + if (localBinPath) { + pathArr.unshift(localBinPath) binExists = true } else if (await fileExists(`${globalBin}/${args[0]}`)) { pathArr.unshift(globalBin) diff --git a/test/index.js b/test/index.js index 725cdbe..3dd1309 100644 --- a/test/index.js +++ b/test/index.js @@ -515,3 +515,77 @@ t.test('sane defaults', async t => { t.ok(fs.statSync(resolve(workdir, 'index.js')).isFile(), 'ran create-index pkg') }) + +t.only('workspaces', async t => { + const pkg = { + name: '@ruyadorno/create-index', + version: '2.0.0', + bin: { + 'create-index': './index.js', + }, + } + const path = t.testdir({ + cache: {}, + node_modules: { + '.bin': {}, + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify(pkg), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('resfile', 'LOCAL PKG')`, + }, + }, + a: t.fixture('symlink', '../a'), + }, + 'package.json': JSON.stringify({ + name: 'project', + workspaces: ['a'], + }), + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + dependencies: { + '@ruyadorno/create-index': '^2.0.0', + }, + }), + } + }) + const runPath = path + const cache = resolve(path, 'cache') + + const executable = + resolve(path, 'node_modules/@ruyadorno/create-index/index.js') + fs.chmodSync(executable, 0o775) + + await binLinks({ + path: resolve(path, 'node_modules/@ruyadorno/create-index'), + pkg, + }) + + // runs at the project level + await libexec({ + ...baseOpts, + args: ['create-index'], + localBin: resolve(path, 'node_modules/.bin'), + cache, + path, + runPath, + }) + + const res = fs.readFileSync(resolve(path, 'resfile')).toString() + t.equal(res, 'LOCAL PKG', 'should run existing bin from project level') + + // runs at the child workspace level + await libexec({ + ...baseOpts, + args: ['create-index'], + cache, + localBin: resolve(path, 'a/node_modules/.bin'), + path: resolve(path, 'a'), + runPath: resolve(path, 'a'), + }) + + const wRes = fs.readFileSync(resolve(path, 'a/resfile')).toString() + t.equal(wRes, 'LOCAL PKG', 'should run existing bin from workspace level') +})