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

Commit 2e418d1

Browse files
authored
feat(local): improve the behavior when calling ./local paths (#48)
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.
1 parent 09bba3b commit 2e418d1

File tree

6 files changed

+91
-27
lines changed

6 files changed

+91
-27
lines changed

child.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ function runCommand (command, opts) {
1818
}`
1919
)
2020
err.exitCode = 127
21+
} else {
22+
err.message = require('./y.js')`Command failed: ${cmd} ${err.message}`
2123
}
2224
throw err
2325
})

index.js

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,11 @@ function ensurePackages (specs, opts) {
123123

124124
module.exports._getExistingPath = getExistingPath
125125
function getExistingPath (command, opts) {
126-
if (opts.cmdHadVersion || opts.packageRequested || opts.ignoreExisting) {
126+
if (opts.isLocal) {
127+
return Promise.resolve(command)
128+
} else if (
129+
opts.cmdHadVersion || opts.packageRequested || opts.ignoreExisting
130+
) {
127131
return Promise.resolve(false)
128132
} else {
129133
return which(command).catch(err => {
@@ -182,9 +186,9 @@ function installPackages (specs, prefix, opts) {
182186

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

213217
module.exports._findNodeScript = findNodeScript
214-
function findNodeScript (existing) {
218+
function findNodeScript (existing, opts) {
215219
if (!existing || process.platform === 'win32') {
216220
return Promise.resolve(false)
217221
} else {
218-
// NOTE: only *nix is supported for process-replacement juggling
219-
const line = '#!/usr/bin/env node\n'
220-
const bytecount = line.length
221-
const buf = Buffer.alloc(bytecount)
222-
return promisify(fs.open)(existing, 'r').then(fd => {
223-
return promisify(fs.read)(fd, buf, 0, bytecount, 0).then(() => {
224-
return promisify(fs.close)(fd)
225-
}, err => {
226-
return promisify(fs.close)(fd).then(() => { throw err })
227-
})
228-
}).then(() => {
229-
return buf.toString('utf8') === line && existing
222+
return promisify(fs.stat)(existing).then(stat => {
223+
if (opts && opts.isLocal && path.extname(existing) === '.js') {
224+
return existing
225+
} else if (opts && opts.isLocal && stat.isDirectory()) {
226+
// npx will execute the directory itself
227+
try {
228+
const pkg = require(path.resolve(existing, 'package.json'))
229+
const target = path.resolve(existing, pkg.bin || pkg.main || 'index.js')
230+
return findNodeScript(target, opts).then(script => {
231+
if (script) {
232+
return script
233+
} else {
234+
throw new Error(Y()`command not found: ${target}`)
235+
}
236+
})
237+
} catch (e) {
238+
throw new Error(Y()`command not found: ${existing}`)
239+
}
240+
} else {
241+
const line = '#!/usr/bin/env node\n'
242+
const bytecount = line.length
243+
const buf = Buffer.alloc(bytecount)
244+
return promisify(fs.open)(existing, 'r').then(fd => {
245+
return promisify(fs.read)(fd, buf, 0, bytecount, 0).then(() => {
246+
return promisify(fs.close)(fd)
247+
}, err => {
248+
return promisify(fs.close)(fd).then(() => { throw err })
249+
})
250+
}).then(() => {
251+
return buf.toString('utf8') === line && existing
252+
})
253+
}
230254
})
231255
}
232256
}

parse-args.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,16 @@ function parseArgs (argv) {
3939
if (cmdIndex) {
4040
const parsed = parser.parse(argv.slice(0, cmdIndex))
4141
const parsedCmd = npa(argv[cmdIndex])
42-
parsed.command = parsed.package
42+
parsed.command = parsed.package && parsedCmd.type !== 'directory'
4343
? argv[cmdIndex]
4444
: guessCmdName(parsedCmd)
45+
parsed.isLocal = parsedCmd.type === 'directory'
4546
parsed.cmdOpts = argv.slice(cmdIndex + 1)
4647
if (typeof parsed.package === 'string') {
4748
parsed.package = [parsed.package]
4849
}
4950
parsed.packageRequested = !!parsed.package
50-
parsed.cmdHadVersion = parsed.package
51+
parsed.cmdHadVersion = parsed.package || parsedCmd.type === 'directory'
5152
? false
5253
: parsedCmd.name !== parsedCmd.raw
5354
const pkg = parsed.package || [argv[cmdIndex]]
@@ -95,13 +96,21 @@ function fastPathArgs (argv) {
9596
} else {
9697
npa = require('npm-package-arg')
9798
parsedCmd = npa(argv[2])
98-
pkg = [parsedCmd.toString()]
99+
if (parsedCmd.type === 'directory') {
100+
pkg = []
101+
} else {
102+
pkg = [parsedCmd.toString()]
103+
}
99104
}
100105
return {
101106
command: guessCmdName(parsedCmd),
102107
cmdOpts: argv.slice(3),
103108
packageRequested: false,
104-
cmdHadVersion: parsedCmd.name !== parsedCmd.raw,
109+
isLocal: parsedCmd.type === 'directory',
110+
cmdHadVersion: (
111+
parsedCmd.name !== parsedCmd.raw &&
112+
parsedCmd.type !== 'directory'
113+
),
105114
package: pkg,
106115
p: pkg,
107116
shell: false,
@@ -129,7 +138,7 @@ function guessCmdName (spec) {
129138
const match = spec.fetchSpec.match(/([a-z0-9-]+)(?:\.git)?$/i)
130139
return match[1]
131140
} else if (spec.type === 'directory') {
132-
return path.basename(spec.fetchSpec)
141+
return spec.raw
133142
} else if (spec.type === 'file' || spec.type === 'remote') {
134143
let ext = path.extname(spec.fetchSpec)
135144
if (ext === '.gz') {

test/guess-command-name.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@ test('guesses git binaries', t => {
2626
t.done()
2727
})
2828

29-
test('guesses local directory binaries', t => {
30-
t.equal(guessCmdName('./foo'), 'foo')
31-
t.equal(guessCmdName('./dir/foo'), 'foo')
32-
t.equal(guessCmdName('../../../dir/foo'), 'foo')
33-
t.equal(guessCmdName('C:\\Program Files\\node\\foo'), 'foo')
29+
test('leaves local directory/file commands intact', t => {
30+
t.equal(guessCmdName('./foo'), './foo')
31+
t.equal(guessCmdName('./dir/foo'), './dir/foo')
32+
t.equal(guessCmdName('../../../dir/foo'), '../../../dir/foo')
33+
t.equal(
34+
guessCmdName('C:\\Program Files\\node\\foo'),
35+
'C:\\Program Files\\node\\foo'
36+
)
3437
t.done()
3538
})
3639

test/index.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ test('getNpmCache', t => {
164164
})
165165

166166
test('findNodeScript', t => {
167-
const scriptPath = path.resolve(__dirname, '..', 'index.js')
167+
const scriptDir = path.resolve(__dirname, '..')
168+
const scriptPath = path.join(scriptDir, 'index.js')
168169
return main._findNodeScript(scriptPath).then(script => {
169170
if (process.platform === 'win32') {
170171
t.notOk(script, 'win32 never detects Node scripts like this')
@@ -178,9 +179,18 @@ test('findNodeScript', t => {
178179
return main._findNodeScript(null).then(bool => {
179180
t.notOk(bool, 'no node script found if existing is null')
180181
})
182+
}).then(() => {
183+
return main._findNodeScript(scriptDir, {isLocal: true}).then(script => {
184+
t.equal(script, scriptPath, 'resolved dir dep to index.js')
185+
})
181186
}).then(() => {
182187
const findScript = requireInject('../index.js', {
183188
fs: {
189+
stat (file, cb) {
190+
cb(null, {
191+
isDirectory () { return !file.indexOf('./') }
192+
})
193+
},
184194
open (file, perm, cb) {
185195
cb(null, file)
186196
},

test/parse-args.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,19 @@ test('allows configuration of npm binary', t => {
140140
t.equal(parsed.npm, './mynpm')
141141
t.done()
142142
})
143+
144+
test('treats directory-type commands specially', t => {
145+
let parsed = parseArgs(['/node', '/npx', './foo'])
146+
t.equal(parsed.command, './foo')
147+
t.deepEqual(parsed.package, [])
148+
t.equal(parsed.packageRequested, false)
149+
t.equal(parsed.cmdHadVersion, false)
150+
t.ok(parsed.isLocal)
151+
parsed = parseArgs(['/node', '/npx', '-p', 'x', '../foo/bar.sh'])
152+
t.equal(parsed.command, '../foo/bar.sh')
153+
t.ok(parsed.isLocal)
154+
t.deepEqual(parsed.package, ['x@latest'])
155+
t.equal(parsed.packageRequested, true)
156+
t.equal(parsed.cmdHadVersion, false)
157+
t.done()
158+
})

0 commit comments

Comments
 (0)