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