Skip to content

Commit 8f2707e

Browse files
committed
feat: load directories.bin as a bin object
Fix: npm/arborist#296 Fix: npm/cli#3446 PR-URL: #4 Credit: @isaacs Close: #4 Reviewed-by: @nlf
1 parent ec08a8d commit 8f2707e

File tree

2 files changed

+108
-1
lines changed

2 files changed

+108
-1
lines changed

index.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,56 @@
11
const {promisify} = require('util')
22
const fs = require('fs')
33
const readFile = promisify(fs.readFile)
4+
const lstat = promisify(fs.lstat)
5+
const readdir = promisify(fs.readdir)
46
const parse = require('json-parse-even-better-errors')
7+
8+
const { resolve, dirname, join, relative } = require('path')
9+
510
const rpj = path => readFile(path, 'utf8')
6-
.then(data => normalize(stripUnderscores(parse(data))))
11+
.then(data => readBinDir(path, normalize(stripUnderscores(parse(data)))))
712
.catch(er => {
813
er.path = path
914
throw er
1015
})
16+
1117
const normalizePackageBin = require('npm-normalize-package-bin')
1218

19+
// load the directories.bin folder as a 'bin' object
20+
const readBinDir = async (path, data) => {
21+
if (data.bin)
22+
return data
23+
24+
const m = data.directories && data.directories.bin
25+
if (!m || typeof m !== 'string')
26+
return data
27+
28+
// cut off any monkey business, like setting directories.bin
29+
// to ../../../etc/passwd or /etc/passwd or something like that.
30+
const root = dirname(path)
31+
const dir = join('.', join('/', m))
32+
data.bin = await walkBinDir(root, dir, {})
33+
return data
34+
}
35+
36+
const walkBinDir = async (root, dir, obj) => {
37+
const entries = await readdir(resolve(root, dir)).catch(() => [])
38+
for (const entry of entries) {
39+
if (entry.charAt(0) === '.')
40+
continue
41+
const f = resolve(root, dir, entry)
42+
// ignore stat errors, weird file types, symlinks, etc.
43+
const st = await lstat(f).catch(() => null)
44+
if (!st)
45+
continue
46+
else if (st.isFile())
47+
obj[entry] = relative(root, f)
48+
else if (st.isDirectory())
49+
await walkBinDir(root, join(dir, entry), obj)
50+
}
51+
return obj
52+
}
53+
1354
// do not preserve _fields set in files, they are sus
1455
const stripUnderscores = data => {
1556
for (const key of Object.keys(data).filter(k => /^_/.test(k)))

test/basic.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const t = require('tap')
2+
23
const rpj = require('../')
34

45
t.test('errors for bad/missing data', async t => {
@@ -273,3 +274,68 @@ t.test('strip _fields', async t => {
273274
// no _lodash
274275
})
275276
})
277+
278+
t.test('load directories.bin', async t => {
279+
const { basename } = require('path')
280+
const fs = require('fs')
281+
const rpj = t.mock('../', {
282+
fs: {
283+
...fs,
284+
lstat: (p, cb) => {
285+
if (basename(p) === 'staterror')
286+
cb(new Error('stat error'))
287+
else
288+
return fs.lstat(p, cb)
289+
},
290+
readdir: (p, cb) => {
291+
if (basename(p) === 'readdirerror') {
292+
cb(new Error('readdir error'))
293+
} else
294+
return fs.readdir(p, cb)
295+
},
296+
},
297+
})
298+
const path = t.testdir({
299+
'package.json': JSON.stringify({
300+
name: 'foo',
301+
version: '1.2.3',
302+
directories: {
303+
bin: 'bin',
304+
},
305+
}),
306+
bin: {
307+
foo: 'foo',
308+
bar: 'bar',
309+
'.ignorethis': 'should be ignored',
310+
linky: t.fixture('symlink', './foo'),
311+
subdir: {
312+
a: 'a',
313+
b: 'b',
314+
sub: {
315+
suba: 'dub',
316+
},
317+
},
318+
staterror: 'do not stat me please',
319+
readdirerror: {
320+
do: 'not',
321+
read: 'this',
322+
dir: 'please',
323+
},
324+
},
325+
})
326+
t.strictSame(await rpj(`${path}/package.json`), {
327+
name: 'foo',
328+
version: '1.2.3',
329+
330+
directories: {
331+
bin: 'bin',
332+
},
333+
bin: {
334+
foo: 'bin/foo',
335+
bar: 'bin/bar',
336+
a: 'bin/subdir/a',
337+
b: 'bin/subdir/b',
338+
suba: 'bin/subdir/sub/suba',
339+
},
340+
})
341+
})

0 commit comments

Comments
 (0)