xref: /third_party/node/deps/npm/lib/commands/link.js (revision 1cb0ef41)
1const fs = require('fs')
2const util = require('util')
3const readdir = util.promisify(fs.readdir)
4const { resolve } = require('path')
5
6const npa = require('npm-package-arg')
7const pkgJson = require('@npmcli/package-json')
8const semver = require('semver')
9
10const reifyFinish = require('../utils/reify-finish.js')
11
12const ArboristWorkspaceCmd = require('../arborist-cmd.js')
13class Link extends ArboristWorkspaceCmd {
14  static description = 'Symlink a package folder'
15  static name = 'link'
16  static usage = [
17    '[<package-spec>]',
18  ]
19
20  static params = [
21    'save',
22    'save-exact',
23    'global',
24    'install-strategy',
25    'legacy-bundling',
26    'global-style',
27    'strict-peer-deps',
28    'package-lock',
29    'omit',
30    'include',
31    'ignore-scripts',
32    'audit',
33    'bin-links',
34    'fund',
35    'dry-run',
36    ...super.params,
37  ]
38
39  static async completion (opts, npm) {
40    const dir = npm.globalDir
41    const files = await readdir(dir)
42    return files.filter(f => !/^[._-]/.test(f))
43  }
44
45  async exec (args) {
46    if (this.npm.global) {
47      throw Object.assign(
48        new Error(
49          'link should never be --global.\n' +
50          'Please re-run this command with --local'
51        ),
52        { code: 'ELINKGLOBAL' }
53      )
54    }
55    // install-links is implicitly false when running `npm link`
56    this.npm.config.set('install-links', false)
57
58    // link with no args: symlink the folder to the global location
59    // link with package arg: symlink the global to the local
60    args = args.filter(a => resolve(a) !== this.npm.prefix)
61    return args.length
62      ? this.linkInstall(args)
63      : this.linkPkg()
64  }
65
66  async linkInstall (args) {
67    // load current packages from the global space,
68    // and then add symlinks installs locally
69    const globalTop = resolve(this.npm.globalDir, '..')
70    const Arborist = require('@npmcli/arborist')
71    const globalOpts = {
72      ...this.npm.flatOptions,
73      Arborist,
74      path: globalTop,
75      global: true,
76      prune: false,
77    }
78    const globalArb = new Arborist(globalOpts)
79
80    // get only current top-level packages from the global space
81    const globals = await globalArb.loadActual({
82      filter: (node, kid) =>
83        !node.isRoot || args.some(a => npa(a).name === kid),
84    })
85
86    // any extra arg that is missing from the current
87    // global space should be reified there first
88    const missing = this.missingArgsFromTree(globals, args)
89    if (missing.length) {
90      await globalArb.reify({
91        ...globalOpts,
92        add: missing,
93      })
94    }
95
96    // get a list of module names that should be linked in the local prefix
97    const names = []
98    for (const a of args) {
99      const arg = npa(a)
100      if (arg.type === 'directory') {
101        const { content } = await pkgJson.normalize(arg.fetchSpec)
102        names.push(content.name)
103      } else {
104        names.push(arg.name)
105      }
106    }
107
108    // npm link should not save=true by default unless you're
109    // using any of --save-dev or other types
110    const save =
111      Boolean(
112        (this.npm.config.find('save') !== 'default' &&
113        this.npm.config.get('save')) ||
114        this.npm.config.get('save-optional') ||
115        this.npm.config.get('save-peer') ||
116        this.npm.config.get('save-dev') ||
117        this.npm.config.get('save-prod')
118      )
119    // create a new arborist instance for the local prefix and
120    // reify all the pending names as symlinks there
121    const localArb = new Arborist({
122      ...this.npm.flatOptions,
123      prune: false,
124      path: this.npm.prefix,
125      save,
126    })
127    await localArb.reify({
128      ...this.npm.flatOptions,
129      prune: false,
130      path: this.npm.prefix,
131      add: names.map(l => `file:${resolve(globalTop, 'node_modules', l).replace(/#/g, '%23')}`),
132      save,
133      workspaces: this.workspaceNames,
134    })
135
136    await reifyFinish(this.npm, localArb)
137  }
138
139  async linkPkg () {
140    const wsp = this.workspacePaths
141    const paths = wsp && wsp.length ? wsp : [this.npm.prefix]
142    const add = paths.map(path => `file:${path.replace(/#/g, '%23')}`)
143    const globalTop = resolve(this.npm.globalDir, '..')
144    const Arborist = require('@npmcli/arborist')
145    const arb = new Arborist({
146      ...this.npm.flatOptions,
147      Arborist,
148      path: globalTop,
149      global: true,
150    })
151    await arb.reify({
152      add,
153    })
154    await reifyFinish(this.npm, arb)
155  }
156
157  // Returns a list of items that can't be fulfilled by
158  // things found in the current arborist inventory
159  missingArgsFromTree (tree, args) {
160    if (tree.isLink) {
161      return this.missingArgsFromTree(tree.target, args)
162    }
163
164    const foundNodes = []
165    const missing = args.filter(a => {
166      const arg = npa(a)
167      const nodes = tree.children.values()
168      const argFound = [...nodes].every(node => {
169        // TODO: write tests for unmatching version specs, this is hard to test
170        // atm but should be simple once we have a mocked registry again
171        if (arg.name !== node.name /* istanbul ignore next */ || (
172          arg.version &&
173          /* istanbul ignore next */
174          !semver.satisfies(node.version, arg.version)
175        )) {
176          foundNodes.push(node)
177          return true
178        }
179      })
180      return argFound
181    })
182
183    // remote nodes from the loaded tree in order
184    // to avoid dropping them later when reifying
185    for (const node of foundNodes) {
186      node.parent = null
187    }
188
189    return missing
190  }
191}
192module.exports = Link
193