-
Notifications
You must be signed in to change notification settings - Fork 16
/
link-gently.js
81 lines (70 loc) · 2.57 KB
/
link-gently.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// if the thing isn't there, skip it
// if there's a non-symlink there already, eexist
// if there's a symlink already, pointing somewhere else, eexist
// if there's a symlink already, pointing into our pkg, remove it first
// then create the symlink
const { promisify } = require('util')
const { resolve, dirname } = require('path')
const mkdirp = require('mkdirp-infer-owner')
const fs = require('fs')
const symlink = promisify(fs.symlink)
const readlink = promisify(fs.readlink)
const lstat = promisify(fs.lstat)
const throwNonEnoent = er => { if (er.code !== 'ENOENT') throw er }
// even in --force mode, we never create a link over a link we've
// already created. you can have multiple packages in a tree trying
// to contend for the same bin, or the same manpage listed multiple times,
// which creates a race condition and nondeterminism.
const seen = new Set()
// disable glob in our rimraf calls
const rimraf = promisify(require('rimraf'))
const rm = path => rimraf(path, { glob: false })
const SKIP = Symbol('skip - missing or already installed')
const CLOBBER = Symbol('clobber - ours or in forceful mode')
const linkGently = async ({path, to, from, absFrom, force}) => {
if (seen.has(to))
return true
seen.add(to)
// if the script or manpage isn't there, just ignore it.
// this arguably *should* be an install error of some sort,
// or at least a warning, but npm has always behaved this
// way in the past, so it'd be a breaking change
return Promise.all([
lstat(absFrom).catch(throwNonEnoent),
lstat(to).catch(throwNonEnoent),
]).then(([stFrom, stTo]) => {
// not present in package, skip it
if (!stFrom)
return SKIP
// exists! maybe clobber if we can
if (stTo) {
if (!stTo.isSymbolicLink())
return force && rm(to).then(() => CLOBBER)
return readlink(to).then(target => {
if (target === from)
return SKIP // skip it, already set up like we want it.
target = resolve(dirname(to), target)
if (target.indexOf(path) === 0 || force)
return rm(to).then(() => CLOBBER)
})
} else {
// doesn't exist, dir might not either
return mkdirp(dirname(to))
}
})
.then(skipOrClobber => {
if (skipOrClobber === SKIP)
return true
return symlink(from, to, 'file').catch(er => {
if (skipOrClobber === CLOBBER || force)
return rm(to).then(() => symlink(from, to, 'file'))
throw er
}).then(() => true)
})
}
const resetSeen = () => {
for (const p of seen) {
seen.delete(p)
}
}
module.exports = Object.assign(linkGently, { resetSeen })