Skip to content
This repository has been archived by the owner on Apr 7, 2021. It is now read-only.

Commit

Permalink
feat(local): improve the behavior when calling ./local paths (#48)
Browse files Browse the repository at this point in the history
Fixes: #49

BREAKING CHANGE: `npx ./something` will now execute `./something` as a
binary or script instead of trying to install it as npm would. Other behavior
related to local path deps has likewise been changed. See
[#49](zkat/npx#49) for a detailed explanation
of all the various cases and how each of them is handled.
  • Loading branch information
zkat authored Jun 24, 2017
1 parent 09bba3b commit 2e418d1
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 27 deletions.
2 changes: 2 additions & 0 deletions child.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ function runCommand (command, opts) {
}`
)
err.exitCode = 127
} else {
err.message = require('./y.js')`Command failed: ${cmd} ${err.message}`
}
throw err
})
Expand Down
56 changes: 40 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ function ensurePackages (specs, opts) {

module.exports._getExistingPath = getExistingPath
function getExistingPath (command, opts) {
if (opts.cmdHadVersion || opts.packageRequested || opts.ignoreExisting) {
if (opts.isLocal) {
return Promise.resolve(command)
} else if (
opts.cmdHadVersion || opts.packageRequested || opts.ignoreExisting
) {
return Promise.resolve(false)
} else {
return which(command).catch(err => {
Expand Down Expand Up @@ -182,9 +186,9 @@ function installPackages (specs, prefix, opts) {

module.exports._execCommand = execCommand
function execCommand (_existing, argv) {
return findNodeScript(_existing).then(existing => {
return findNodeScript(_existing, argv).then(existing => {
const Module = require('module')
if (existing && Module.runMain && !argv.shell) {
if (existing && Module.runMain && !argv.shell && existing !== __filename) {
// let it take over the process. This means we can skip node startup!
if (!argv.noYargs) {
// blow away built-up yargs crud
Expand All @@ -211,22 +215,42 @@ function execCommand (_existing, argv) {
}

module.exports._findNodeScript = findNodeScript
function findNodeScript (existing) {
function findNodeScript (existing, opts) {
if (!existing || process.platform === 'win32') {
return Promise.resolve(false)
} else {
// NOTE: only *nix is supported for process-replacement juggling
const line = '#!/usr/bin/env node\n'
const bytecount = line.length
const buf = Buffer.alloc(bytecount)
return promisify(fs.open)(existing, 'r').then(fd => {
return promisify(fs.read)(fd, buf, 0, bytecount, 0).then(() => {
return promisify(fs.close)(fd)
}, err => {
return promisify(fs.close)(fd).then(() => { throw err })
})
}).then(() => {
return buf.toString('utf8') === line && existing
return promisify(fs.stat)(existing).then(stat => {
if (opts && opts.isLocal && path.extname(existing) === '.js') {
return existing
} else if (opts && opts.isLocal && stat.isDirectory()) {
// npx will execute the directory itself
try {
const pkg = require(path.resolve(existing, 'package.json'))
const target = path.resolve(existing, pkg.bin || pkg.main || 'index.js')
return findNodeScript(target, opts).then(script => {
if (script) {
return script
} else {
throw new Error(Y()`command not found: ${target}`)
}
})
} catch (e) {
throw new Error(Y()`command not found: ${existing}`)
}
} else {
const line = '#!/usr/bin/env node\n'
const bytecount = line.length
const buf = Buffer.alloc(bytecount)
return promisify(fs.open)(existing, 'r').then(fd => {
return promisify(fs.read)(fd, buf, 0, bytecount, 0).then(() => {
return promisify(fs.close)(fd)
}, err => {
return promisify(fs.close)(fd).then(() => { throw err })
})
}).then(() => {
return buf.toString('utf8') === line && existing
})
}
})
}
}
Expand Down
19 changes: 14 additions & 5 deletions parse-args.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@ function parseArgs (argv) {
if (cmdIndex) {
const parsed = parser.parse(argv.slice(0, cmdIndex))
const parsedCmd = npa(argv[cmdIndex])
parsed.command = parsed.package
parsed.command = parsed.package && parsedCmd.type !== 'directory'
? argv[cmdIndex]
: guessCmdName(parsedCmd)
parsed.isLocal = parsedCmd.type === 'directory'
parsed.cmdOpts = argv.slice(cmdIndex + 1)
if (typeof parsed.package === 'string') {
parsed.package = [parsed.package]
}
parsed.packageRequested = !!parsed.package
parsed.cmdHadVersion = parsed.package
parsed.cmdHadVersion = parsed.package || parsedCmd.type === 'directory'
? false
: parsedCmd.name !== parsedCmd.raw
const pkg = parsed.package || [argv[cmdIndex]]
Expand Down Expand Up @@ -95,13 +96,21 @@ function fastPathArgs (argv) {
} else {
npa = require('npm-package-arg')
parsedCmd = npa(argv[2])
pkg = [parsedCmd.toString()]
if (parsedCmd.type === 'directory') {
pkg = []
} else {
pkg = [parsedCmd.toString()]
}
}
return {
command: guessCmdName(parsedCmd),
cmdOpts: argv.slice(3),
packageRequested: false,
cmdHadVersion: parsedCmd.name !== parsedCmd.raw,
isLocal: parsedCmd.type === 'directory',
cmdHadVersion: (
parsedCmd.name !== parsedCmd.raw &&
parsedCmd.type !== 'directory'
),
package: pkg,
p: pkg,
shell: false,
Expand Down Expand Up @@ -129,7 +138,7 @@ function guessCmdName (spec) {
const match = spec.fetchSpec.match(/([a-z0-9-]+)(?:\.git)?$/i)
return match[1]
} else if (spec.type === 'directory') {
return path.basename(spec.fetchSpec)
return spec.raw
} else if (spec.type === 'file' || spec.type === 'remote') {
let ext = path.extname(spec.fetchSpec)
if (ext === '.gz') {
Expand Down
13 changes: 8 additions & 5 deletions test/guess-command-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ test('guesses git binaries', t => {
t.done()
})

test('guesses local directory binaries', t => {
t.equal(guessCmdName('./foo'), 'foo')
t.equal(guessCmdName('./dir/foo'), 'foo')
t.equal(guessCmdName('../../../dir/foo'), 'foo')
t.equal(guessCmdName('C:\\Program Files\\node\\foo'), 'foo')
test('leaves local directory/file commands intact', t => {
t.equal(guessCmdName('./foo'), './foo')
t.equal(guessCmdName('./dir/foo'), './dir/foo')
t.equal(guessCmdName('../../../dir/foo'), '../../../dir/foo')
t.equal(
guessCmdName('C:\\Program Files\\node\\foo'),
'C:\\Program Files\\node\\foo'
)
t.done()
})

Expand Down
12 changes: 11 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ test('getNpmCache', t => {
})

test('findNodeScript', t => {
const scriptPath = path.resolve(__dirname, '..', 'index.js')
const scriptDir = path.resolve(__dirname, '..')
const scriptPath = path.join(scriptDir, 'index.js')
return main._findNodeScript(scriptPath).then(script => {
if (process.platform === 'win32') {
t.notOk(script, 'win32 never detects Node scripts like this')
Expand All @@ -178,9 +179,18 @@ test('findNodeScript', t => {
return main._findNodeScript(null).then(bool => {
t.notOk(bool, 'no node script found if existing is null')
})
}).then(() => {
return main._findNodeScript(scriptDir, {isLocal: true}).then(script => {
t.equal(script, scriptPath, 'resolved dir dep to index.js')
})
}).then(() => {
const findScript = requireInject('../index.js', {
fs: {
stat (file, cb) {
cb(null, {
isDirectory () { return !file.indexOf('./') }
})
},
open (file, perm, cb) {
cb(null, file)
},
Expand Down
16 changes: 16 additions & 0 deletions test/parse-args.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,19 @@ test('allows configuration of npm binary', t => {
t.equal(parsed.npm, './mynpm')
t.done()
})

test('treats directory-type commands specially', t => {
let parsed = parseArgs(['/node', '/npx', './foo'])
t.equal(parsed.command, './foo')
t.deepEqual(parsed.package, [])
t.equal(parsed.packageRequested, false)
t.equal(parsed.cmdHadVersion, false)
t.ok(parsed.isLocal)
parsed = parseArgs(['/node', '/npx', '-p', 'x', '../foo/bar.sh'])
t.equal(parsed.command, '../foo/bar.sh')
t.ok(parsed.isLocal)
t.deepEqual(parsed.package, ['x@latest'])
t.equal(parsed.packageRequested, true)
t.equal(parsed.cmdHadVersion, false)
t.done()
})

0 comments on commit 2e418d1

Please sign in to comment.