diff --git a/README.md b/README.md index a436c9a..fb7a771 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 to start scanning for bin files **String**, 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..a115be1 --- /dev/null +++ b/lib/file-exists.js @@ -0,0 +1,29 @@ +const { resolve } = require('path') +const { promisify } = require('util') +const stat = promisify(require('fs').stat) +const walkUp = require('walk-up-path') + +const fileExists = (file) => stat(file) + .then((stat) => stat.isFile()) + .catch(() => false) + +const localFileExists = async (dir, binName, root = '/') => { + root = resolve(root).toLowerCase() + + for (const path of walkUp(resolve(dir))) { + const binDir = resolve(path, 'node_modules', '.bin') + + if (await fileExists(resolve(binDir, binName))) + return binDir + + if (path.toLowerCase() === root) + return false + } + + return false +} + +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/package-lock.json b/package-lock.json index 5480adf..ad425b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "pacote": "^11.3.1", "proc-log": "^1.0.0", "read": "^1.0.7", - "read-package-json-fast": "^2.0.2" + "read-package-json-fast": "^2.0.2", + "walk-up-path": "^1.0.0" }, "devDependencies": { "bin-links": "^2.2.1", diff --git a/package.json b/package.json index 4c03161..3a3ae4f 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "pacote": "^11.3.1", "proc-log": "^1.0.0", "read": "^1.0.7", - "read-package-json-fast": "^2.0.2" + "read-package-json-fast": "^2.0.2", + "walk-up-path": "^1.0.0" } } diff --git a/test/file-exists.js b/test/file-exists.js new file mode 100644 index 0000000..9a1f53c --- /dev/null +++ b/test/file-exists.js @@ -0,0 +1,14 @@ +const t = require('tap') +const { localFileExists } = require('../lib/file-exists.js') + +t.test('missing root value', async t => { + const dir = t.testdir({ + b: { + c: {}, + }, + }) + + // root value a is not part of the file system hierarchy + const fileExists = await localFileExists(dir, 'foo', 'a') + t.equal(fileExists, false, 'should return false on missing root') +}) 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') +})