1// link with no args: symlink the folder to the global location 2// link with package arg: symlink the global to the local 3 4var npm = require('./npm.js') 5var symlink = require('./utils/link.js') 6var fs = require('graceful-fs') 7var log = require('npmlog') 8var asyncMap = require('slide').asyncMap 9var chain = require('slide').chain 10var path = require('path') 11var build = require('./build.js') 12var npa = require('npm-package-arg') 13var usage = require('./utils/usage') 14var output = require('./utils/output.js') 15 16module.exports = link 17 18link.usage = usage( 19 'link', 20 'npm link (in package dir)' + 21 '\nnpm link [<@scope>/]<pkg>[@<version>]' 22) 23 24link.completion = function (opts, cb) { 25 var dir = npm.globalDir 26 fs.readdir(dir, function (er, files) { 27 cb(er, files.filter(function (f) { 28 return !f.match(/^[._-]/) 29 })) 30 }) 31} 32 33function link (args, cb) { 34 if (process.platform === 'win32') { 35 var semver = require('semver') 36 if (!semver.gte(process.version, '0.7.9')) { 37 var msg = 'npm link not supported on windows prior to node 0.7.9' 38 var e = new Error(msg) 39 e.code = 'ENOTSUP' 40 e.errno = require('constants').ENOTSUP // eslint-disable-line node/no-deprecated-api 41 return cb(e) 42 } 43 } 44 45 if (npm.config.get('global')) { 46 return cb(new Error( 47 'link should never be --global.\n' + 48 'Please re-run this command with --local' 49 )) 50 } 51 52 if (args.length === 1 && args[0] === '.') args = [] 53 if (args.length) return linkInstall(args, cb) 54 linkPkg(npm.prefix, cb) 55} 56 57function parentFolder (id, folder) { 58 if (id[0] === '@') { 59 return path.resolve(folder, '..', '..') 60 } else { 61 return path.resolve(folder, '..') 62 } 63} 64 65function linkInstall (pkgs, cb) { 66 asyncMap(pkgs, function (pkg, cb) { 67 var t = path.resolve(npm.globalDir, '..') 68 var pp = path.resolve(npm.globalDir, pkg) 69 var rp = null 70 var target = path.resolve(npm.dir, pkg) 71 72 function n (er, data) { 73 if (er) return cb(er, data) 74 // we want the ONE thing that was installed into the global dir 75 var installed = data.filter(function (info) { 76 var id = info[0] 77 var folder = info[1] 78 return parentFolder(id, folder) === npm.globalDir 79 }) 80 var id = installed[0][0] 81 pp = installed[0][1] 82 var what = npa(id) 83 pkg = what.name 84 target = path.resolve(npm.dir, pkg) 85 next() 86 } 87 88 // if it's a folder, a random not-installed thing, or not a scoped package, 89 // then link or install it first 90 if (pkg[0] !== '@' && (pkg.indexOf('/') !== -1 || pkg.indexOf('\\') !== -1)) { 91 return fs.lstat(path.resolve(pkg), function (er, st) { 92 if (er || !st.isDirectory()) { 93 npm.commands.install(t, pkg, n) 94 } else { 95 rp = path.resolve(pkg) 96 linkPkg(rp, n) 97 } 98 }) 99 } 100 101 fs.lstat(pp, function (er, st) { 102 if (er) { 103 rp = pp 104 return npm.commands.install(t, [pkg], n) 105 } else if (!st.isSymbolicLink()) { 106 rp = pp 107 next() 108 } else { 109 return fs.realpath(pp, function (er, real) { 110 if (er) log.warn('invalid symbolic link', pkg) 111 else rp = real 112 next() 113 }) 114 } 115 }) 116 117 function next () { 118 if (npm.config.get('dry-run')) return resultPrinter(pkg, pp, target, rp, cb) 119 chain( 120 [ 121 [ function (cb) { 122 log.verbose('link', 'symlinking %s to %s', pp, target) 123 cb() 124 } ], 125 [symlink, pp, target, false, false], 126 // do not run any scripts 127 rp && [build, [target], npm.config.get('global'), build._noLC, true], 128 [resultPrinter, pkg, pp, target, rp] 129 ], 130 cb 131 ) 132 } 133 }, cb) 134} 135 136function linkPkg (folder, cb_) { 137 var me = folder || npm.prefix 138 var readJson = require('read-package-json') 139 140 log.verbose('linkPkg', folder) 141 142 readJson(path.resolve(me, 'package.json'), function (er, d) { 143 function cb (er) { 144 return cb_(er, [[d && d._id, target, null, null]]) 145 } 146 if (er) return cb(er) 147 if (!d.name) { 148 er = new Error('Package must have a name field to be linked') 149 return cb(er) 150 } 151 var target = path.resolve(npm.globalDir, d.name) 152 if (npm.config.get('dry-run')) return resultPrinter(path.basename(me), me, target, cb) 153 symlink(me, target, false, true, function (er) { 154 if (er) return cb(er) 155 log.verbose('link', 'build target', target) 156 // also install missing dependencies. 157 npm.commands.install(me, [], function (er) { 158 if (er) return cb(er) 159 // build the global stuff. Don't run *any* scripts, because 160 // install command already will have done that. 161 build([target], true, build._noLC, true, function (er) { 162 if (er) return cb(er) 163 resultPrinter(path.basename(me), me, target, cb) 164 }) 165 }) 166 }) 167 }) 168} 169 170function resultPrinter (pkg, src, dest, rp, cb) { 171 if (typeof cb !== 'function') { 172 cb = rp 173 rp = null 174 } 175 var where = dest 176 rp = (rp || '').trim() 177 src = (src || '').trim() 178 // XXX If --json is set, then look up the data from the package.json 179 if (npm.config.get('parseable')) { 180 return parseableOutput(dest, rp || src, cb) 181 } 182 if (rp === src) rp = null 183 output(where + ' -> ' + src + (rp ? ' -> ' + rp : '')) 184 cb() 185} 186 187function parseableOutput (dest, rp, cb) { 188 // XXX this should match ls --parseable and install --parseable 189 // look up the data from package.json, format it the same way. 190 // 191 // link is always effectively 'long', since it doesn't help much to 192 // *just* print the target folder. 193 // However, we don't actually ever read the version number, so 194 // the second field is always blank. 195 output(dest + '::' + rp) 196 cb() 197} 198