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