All files / lib link.js

100% Statements 61/61
100% Branches 13/13
100% Functions 15/15
100% Lines 55/55

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140    1x 1x   1x 1x 1x 1x   1x 1x 1x   1x 1x 4x     1x           4x   1x 4x 1x                     6x 3x             1x 2x 2x 6x 6x 6x     15x       10x 10x     6x         2x 10x   2x     1x     2x 2x           2x     2x   21x         2x 2x 1x             2x 2x 6x 6x                 2x       2x 6x     2x     1x 1x 1x         1x 1x     1x  
'use strict'
 
const { readdir } = require('fs')
const { resolve } = require('path')
 
const Arborist = require('@npmcli/arborist')
const npa = require('npm-package-arg')
const rpj = require('read-package-json-fast')
const semver = require('semver')
 
const npm = require('./npm.js')
const usageUtil = require('./utils/usage.js')
const reifyFinish = require('./utils/reify-finish.js')
 
const completion = (opts, cb) => {
  const dir = npm.globalDir
  readdir(dir, (er, files) => cb(er, files.filter(f => !/^[._-]/.test(f))))
}
 
const usage = usageUtil(
  'link',
  'npm link (in package dir)' +
  '\nnpm link [<@scope>/]<pkg>[@<version>]'
)
 
const cmd = (args, cb) => link(args).then(() => cb()).catch(cb)
 
const link = async args => {
  if (npm.config.get('global')) {
    throw Object.assign(
      new Error(
        'link should never be --global.\n' +
        'Please re-run this command with --local'
      ),
      { code: 'ELINKGLOBAL' }
    )
  }
 
  // link with no args: symlink the folder to the global location
  // link with package arg: symlink the global to the local
  args = args.filter(a => resolve(a) !== npm.prefix)
  return args.length
    ? linkInstall(args)
    : linkPkg()
}
 
// Returns a list of items that can't be fulfilled by
// things found in the current arborist inventory
const missingArgsFromTree = (tree, args) => {
  const foundNodes = []
  const missing = args.filter(a => {
    const arg = npa(a)
    const nodes = tree.children.values()
    const argFound = [...nodes].every(node => {
      // TODO: write tests for unmatching version specs, this is hard to test
      // atm but should be simple once we have a mocked registry again
      if (arg.name !== node.name /* istanbul ignore next */ || (
        arg.version &&
        !semver.satisfies(node.version, arg.version)
      )) {
        foundNodes.push(node)
        return true
      }
    })
    return argFound
  })
 
  // remote nodes from the loaded tree in order
  // to avoid dropping them later when reifying
  for (const node of foundNodes)
    node.parent = null
 
  return missing
}
 
const linkInstall = async args => {
  // load current packages from the global space,
  // and then add symlinks installs locally
  const globalTop = resolve(npm.globalDir, '..')
  const globalOpts = {
    ...npm.flatOptions,
    path: globalTop,
    global: true,
    prune: false,
  }
  const globalArb = new Arborist(globalOpts)
 
  // get only current top-level packages from the global space
  const globals = await globalArb.loadActual({
    filter: (node, kid) =>
      !node.isRoot || args.some(a => npa(a).name === kid),
  })
 
  // any extra arg that is missing from the current
  // global space should be reified there first
  const missing = missingArgsFromTree(globals, args)
  if (missing.length) {
    await globalArb.reify({
      ...globalOpts,
      add: missing,
    })
  }
 
  // get a list of module names that should be linked in the local prefix
  const names = []
  for (const a of args) {
    const arg = npa(a)
    names.push(
      arg.type === 'directory'
        ? (await rpj(resolve(arg.fetchSpec, 'package.json'))).name
        : arg.name
    )
  }
 
  // create a new arborist instance for the local prefix and
  // reify all the pending names as symlinks there
  const localArb = new Arborist({
    ...npm.flatOptions,
    path: npm.prefix,
  })
  await localArb.reify({
    add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`),
  })
 
  await reifyFinish(localArb)
}
 
const linkPkg = async () => {
  const globalTop = resolve(npm.globalDir, '..')
  const arb = new Arborist({
    ...npm.flatOptions,
    path: globalTop,
    global: true,
  })
  await arb.reify({ add: [`file:${npm.prefix}`] })
  await reifyFinish(arb)
}
 
module.exports = Object.assign(cmd, { completion, usage })