• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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