Skip to content
This repository has been archived by the owner on Nov 8, 2022. It is now read-only.

feat: add option to gently create bin links/shims #7

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
language: node_js
sudo: false

node_js:
- "8"
- "6"
- "4"
- node
- 12
- 10
- 8
- 6
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
const rm = require('./lib/rm.js')
const link = require('./lib/link.js')
const mkdir = require('./lib/mkdir.js')
const binLink = require('./lib/bin-link.js')

exports = module.exports = {
rm: rm,
link: link.link,
linkIfExists: link.linkIfExists,
mkdir: mkdir
mkdir: mkdir,
binLink: binLink
}
96 changes: 96 additions & 0 deletions lib/bin-link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use strict'
// calls linkIfExists on unix, or cmdShimIfExists on Windows
// reads the cmd shim to ensure it's where we need it to be in the case of
// top level global packages

const readCmdShim = require('read-cmd-shim')
const cmdShim = require('cmd-shim')
const {linkIfExists} = require('./link.js')

const binLink = (from, to, opts, cb) => {
// just for testing
const platform = opts._FAKE_PLATFORM_ || process.platform
if (platform !== 'win32') {
return linkIfExists(from, to, opts, cb)
}

if (!opts.clobberLinkGently ||
opts.force === true ||
!opts.gently ||
typeof opts.gently !== 'string') {
// easy, just go ahead and delete anything in the way
return cmdShim.ifExists(from, to, cb)
}

// read all three shim targets
// if any exist, and are not a shim to our gently folder, then
// exit with a simulated EEXIST error.

const shimFiles = [
to,
to + '.cmd',
to + '.ps1'
]

// call this once we've checked all three, if we're good
const done = () => cmdShim.ifExists(from, to, cb)
const then = times(3, done, cb)
shimFiles.forEach(to => isClobberable(from, to, opts, then))
}

const times = (n, ok, cb) => {
let errState = null
return er => {
if (!errState) {
if (er) {
cb(errState = er)
} else if (--n === 0) {
ok()
}
}
}
}

const isClobberable = (from, to, opts, cb) => {
readCmdShim(to, (er, target) => {
// either going to get an error, or the target of where this
// cmd shim points.
// shim, not in opts.gently: simulate EEXIST
// not a shim: simulate EEXIST
// ENOENT: fine, move forward
// shim in opts.gently: fine
if (er) {
switch (er.code) {
case 'ENOENT':
// totally fine, nothing there to clobber
return cb()
case 'ENOTASHIM':
// something is there, and it's not one of ours
return cb(simulateEEXIST(from, to))
default:
// would probably fail this way later anyway
// can't read the file, likely can't write it either
return cb(er)
}
}
// no error, check the target
if (target.indexOf(opts.gently) !== 0) {
return cb(simulateEEXIST(from, to))
}
// ok! it's one of ours.
return cb()
})
}

const simulateEEXIST = (from, to) => {
// simulate the EEXIST we'd get from fs.symlink to the file
const err = new Error('EEXIST: file already exists, cmd shim \'' +
from + '\' -> \'' + to + '\'')

err.code = 'EEXIST'
err.path = from
err.dest = to
return err
}

module.exports = binLink
54 changes: 53 additions & 1 deletion lib/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,22 @@ exports = module.exports = {
}

function linkIfExists (from, to, opts, cb) {
opts.currentIsLink = false
opts.currentExists = false
fs.stat(from, function (er) {
if (er) return cb()
fs.readlink(to, function (er, fromOnDisk) {
if (!er || er.code !== 'ENOENT') {
opts.currentExists = true
}
// if the link already exists and matches what we would do,
// we don't need to do anything
if (!er) {
opts.currentIsLink = true
var toDir = path.dirname(to)
var absoluteFrom = path.resolve(toDir, from)
var absoluteFromOnDisk = path.resolve(toDir, fromOnDisk)
opts.currentTarget = absoluteFromOnDisk
if (absoluteFrom === absoluteFromOnDisk) return cb()
}
link(from, to, opts, cb)
Expand Down Expand Up @@ -58,7 +65,7 @@ function link (from, to, opts, cb) {
const tasks = [
[ensureFromIsNotSource, absTarget, to],
[fs, 'stat', absTarget],
[rm, to, opts],
[clobberLinkGently, from, to, opts],
[mkdir, path.dirname(to)],
[fs, 'symlink', target, to, 'junction']
]
Expand All @@ -72,3 +79,48 @@ function link (from, to, opts, cb) {
})
}
}

exports._clobberLinkGently = clobberLinkGently
function clobberLinkGently (from, to, opts, cb) {
if (opts.currentExists === false) {
// nothing to clobber!
opts.log.silly('gently link', 'link does not already exist', {
link: to,
target: from
})
return cb()
}

if (!opts.clobberLinkGently ||
opts.force === true ||
!opts.gently ||
typeof opts.gently !== 'string') {
opts.log.silly('gently link', 'deleting existing link forcefully', {
link: to,
target: from,
force: opts.force,
gently: opts.gently,
clobberLinkGently: opts.clobberLinkGently
})
return rm(to, opts, cb)
}

if (!opts.currentIsLink) {
opts.log.verbose('gently link', 'cannot remove, not a link', to)
// don't delete. it'll fail with EEXIST when it tries to symlink.
return cb()
}

if (opts.currentTarget.indexOf(opts.gently) === 0) {
opts.log.silly('gently link', 'delete existing link', to)
return rm(to, opts, cb)
} else {
opts.log.verbose('gently link', 'refusing to delete existing link', {
link: to,
currentTarget: opts.currentTarget,
newTarget: from,
gently: opts.gently
})
return cb()
}
}
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"scripts": {
"prerelease": "npm t",
"postrelease": "npm publish && git push --follow-tags",
"pretest": "standard",
"posttest": "standard",
"release": "standard-version -s",
"test": "tap -J --nyc-arg=--all --coverage test/*.js test/**/*.js",
"test": "tap -J --nyc-arg=--all --coverage test",
"update-coc": "weallbehave -o . && git add CODE_OF_CONDUCT.md && git commit -m 'docs(coc): updated CODE_OF_CONDUCT.md'",
"update-contrib": "weallcontribute -o . && git add CONTRIBUTING.md && git commit -m 'docs(contributing): updated CONTRIBUTING.md'"
},
Expand All @@ -30,6 +30,7 @@
"dependencies": {
"aproba": "^1.1.2",
"chownr": "^1.1.2",
"cmd-shim": "^3.0.3",
"fs-vacuum": "^1.2.10",
"graceful-fs": "^4.1.11",
"iferr": "^0.1.5",
Expand Down
9 changes: 9 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const t = require('tap')
const index = require('../')
t.match(index, {
rm: Function,
link: Function,
linkIfExists: Function,
mkdir: Function,
binLink: Function
}, 'exports all the functions')
Loading