xref: /third_party/node/deps/npm/test/lib/commands/link.js (revision 1cb0ef41)
1const t = require('tap')
2const { resolve, join } = require('path')
3const fs = require('fs')
4const Arborist = require('@npmcli/arborist')
5const { cleanCwd } = require('../../fixtures/clean-snapshot.js')
6const mockNpm = require('../../fixtures/mock-npm')
7
8t.cleanSnapshot = (str) => cleanCwd(str)
9
10const mockLink = async (t, { globalPrefixDir, ...opts } = {}) => {
11  const mock = await mockNpm(t, {
12    ...opts,
13    command: 'link',
14    globalPrefixDir,
15    mocks: {
16      ...opts.mocks,
17      '{LIB}/utils/reify-output.js': async () => {},
18    },
19  })
20
21  const printLinks = async ({ global = false } = {}) => {
22    let res = ''
23    const arb = new Arborist(global ? {
24      path: resolve(mock.npm.globalDir, '..'),
25      global: true,
26    } : { path: mock.prefix })
27    const tree = await arb.loadActual()
28    const linkedItems = [...tree.inventory.values()]
29      .sort((a, b) => a.pkgid.localeCompare(b.pkgid, 'en'))
30    for (const item of linkedItems) {
31      if (item.isLink) {
32        res += `${item.path} -> ${item.target.path}\n`
33      }
34    }
35    return res
36  }
37
38  return {
39    ...mock,
40    printLinks,
41  }
42}
43
44t.test('link to globalDir when in current working dir of pkg and no args', async t => {
45  const { link, printLinks } = await mockLink(t, {
46    globalPrefixDir: {
47      node_modules: {
48        a: {
49          'package.json': JSON.stringify({
50            name: 'a',
51            version: '1.0.0',
52          }),
53        },
54      },
55    },
56    prefixDir: {
57      'package.json': JSON.stringify({
58        name: 'test-pkg-link',
59        version: '1.0.0',
60      }),
61    },
62  })
63
64  await link.exec()
65  t.matchSnapshot(await printLinks({ global: true }), 'should create a global link to current pkg')
66})
67
68t.test('link ws to globalDir when workspace specified and no args', async t => {
69  const { link, printLinks } = await mockLink(t, {
70    globalPrefixDir: {
71      node_modules: {
72        a: {
73          'package.json': JSON.stringify({
74            name: 'a',
75            version: '1.0.0',
76          }),
77        },
78      },
79    },
80    prefixDir: {
81      'package.json': JSON.stringify({
82        name: 'test-pkg-link',
83        version: '1.0.0',
84        workspaces: ['packages/*'],
85      }),
86      packages: {
87        a: {
88          'package.json': JSON.stringify({
89            name: 'a',
90            version: '1.0.0',
91          }),
92        },
93      },
94    },
95    config: { workspace: 'a' },
96  })
97
98  await link.exec()
99  t.matchSnapshot(await printLinks({ global: true }), 'should create a global link to current pkg')
100})
101
102t.test('link global linked pkg to local nm when using args', async t => {
103  const { link, printLinks } = await mockLink(t, {
104    globalPrefixDir: {
105      node_modules: {
106        '@myscope': {
107          foo: {
108            'package.json': JSON.stringify({
109              name: '@myscope/foo',
110              version: '1.0.0',
111            }),
112          },
113          bar: {
114            'package.json': JSON.stringify({
115              name: '@myscope/bar',
116              version: '1.0.0',
117            }),
118          },
119          linked: t.fixture('symlink', '../../../other/scoped-linked'),
120        },
121        a: {
122          'package.json': JSON.stringify({
123            name: 'a',
124            version: '1.0.0',
125          }),
126        },
127        b: {
128          'package.json': JSON.stringify({
129            name: 'b',
130            version: '1.0.0',
131          }),
132        },
133        'test-pkg-link': t.fixture('symlink', '../../other/test-pkg-link'),
134      },
135    },
136    otherDirs: {
137      'test-pkg-link': {
138        'package.json': JSON.stringify({
139          name: 'test-pkg-link',
140          version: '1.0.0',
141        }),
142      },
143      'link-me-too': {
144        'package.json': JSON.stringify({
145          name: 'link-me-too',
146          version: '1.0.0',
147        }),
148      },
149      'scoped-linked': {
150        'package.json': JSON.stringify({
151          name: '@myscope/linked',
152          version: '1.0.0',
153        }),
154      },
155    },
156    prefixDir: {
157      'package.json': JSON.stringify({
158        name: 'my-project',
159        version: '1.0.0',
160        dependencies: {
161          foo: '^1.0.0',
162        },
163      }),
164      node_modules: {
165        foo: {
166          'package.json': JSON.stringify({
167            name: 'foo',
168            version: '1.0.0',
169          }),
170        },
171      },
172    },
173  })
174
175  // installs examples for:
176  // - test-pkg-link: pkg linked to globalDir from local fs
177  // - @myscope/linked: scoped pkg linked to globalDir from local fs
178  // - @myscope/bar: prev installed scoped package available in globalDir
179  // - a: prev installed package available in globalDir
180  // - file:./link-me-too: pkg that needs to be reified in globalDir first
181  await link.exec([
182    'test-pkg-link',
183    '@myscope/linked',
184    '@myscope/bar',
185    'a',
186    'file:../other/link-me-too',
187  ])
188
189  t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg')
190})
191
192t.test('link global linked pkg to local workspace using args', async t => {
193  const { link, printLinks } = await mockLink(t, {
194    globalPrefixDir: {
195      node_modules: {
196        '@myscope': {
197          foo: {
198            'package.json': JSON.stringify({
199              name: '@myscope/foo',
200              version: '1.0.0',
201            }),
202          },
203          bar: {
204            'package.json': JSON.stringify({
205              name: '@myscope/bar',
206              version: '1.0.0',
207            }),
208          },
209          linked: t.fixture('symlink', '../../../other/scoped-linked'),
210        },
211        a: {
212          'package.json': JSON.stringify({
213            name: 'a',
214            version: '1.0.0',
215          }),
216        },
217        b: {
218          'package.json': JSON.stringify({
219            name: 'b',
220            version: '1.0.0',
221          }),
222        },
223        'test-pkg-link': t.fixture('symlink', '../../other/test-pkg-link'),
224      },
225    },
226    otherDirs: {
227      'test-pkg-link': {
228        'package.json': JSON.stringify({
229          name: 'test-pkg-link',
230          version: '1.0.0',
231        }),
232      },
233      'link-me-too': {
234        'package.json': JSON.stringify({
235          name: 'link-me-too',
236          version: '1.0.0',
237        }),
238      },
239      'scoped-linked': {
240        'package.json': JSON.stringify({
241          name: '@myscope/linked',
242          version: '1.0.0',
243        }),
244      },
245    },
246    prefixDir: {
247      'package.json': JSON.stringify({
248        name: 'my-project',
249        version: '1.0.0',
250        workspaces: ['packages/*'],
251      }),
252      packages: {
253        x: {
254          'package.json': JSON.stringify({
255            name: 'x',
256            version: '1.0.0',
257            dependencies: {
258              foo: '^1.0.0',
259            },
260          }),
261        },
262      },
263      node_modules: {
264        foo: {
265          'package.json': JSON.stringify({
266            name: 'foo',
267            version: '1.0.0',
268          }),
269        },
270      },
271    },
272    config: { workspace: 'x' },
273  })
274
275  // installs examples for:
276  // - test-pkg-link: pkg linked to globalDir from local fs
277  // - @myscope/linked: scoped pkg linked to globalDir from local fs
278  // - @myscope/bar: prev installed scoped package available in globalDir
279  // - a: prev installed package available in globalDir
280  // - file:./link-me-too: pkg that needs to be reified in globalDir first
281  await link.exec([
282    'test-pkg-link',
283    '@myscope/linked',
284    '@myscope/bar',
285    'a',
286    'file:../other/link-me-too',
287  ])
288
289  t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg')
290})
291
292t.test('link pkg already in global space', async t => {
293  const { npm, link, printLinks, prefix } = await mockLink(t, {
294    globalPrefixDir: {
295      node_modules: {
296        '@myscope': {
297          linked: t.fixture('symlink', '../../../other/scoped-linked'),
298        },
299      },
300    },
301    otherDirs: {
302      'scoped-linked': {
303        'package.json': JSON.stringify({
304          name: '@myscope/linked',
305          version: '1.0.0',
306        }),
307      },
308    },
309    prefixDir: {
310      'package.json': JSON.stringify({
311        name: 'my-project',
312        version: '1.0.0',
313      }),
314    },
315  })
316
317  // XXX: how to convert this to a config that gets passed in?
318  npm.config.find = () => 'default'
319
320  await link.exec(['@myscope/linked'])
321
322  t.equal(
323    require(resolve(prefix, 'package.json')).dependencies,
324    undefined,
325    'should not save to package.json upon linking'
326  )
327
328  t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg')
329})
330
331t.test('link pkg already in global space when prefix is a symlink', async t => {
332  const { npm, link, printLinks, prefix } = await mockLink(t, {
333    globalPrefixDir: t.fixture('symlink', './other/real-global-prefix'),
334    otherDirs: {
335      // mockNpm does this automatically but only for globalPrefixDir so here we
336      // need to do it manually since we are making a symlink somewhere else
337      'real-global-prefix': mockNpm.setGlobalNodeModules({
338        node_modules: {
339          '@myscope': {
340            linked: t.fixture('symlink', '../../../scoped-linked'),
341          },
342        },
343      }),
344      'scoped-linked': {
345        'package.json': JSON.stringify({
346          name: '@myscope/linked',
347          version: '1.0.0',
348        }),
349      },
350    },
351    prefixDir: {
352      'package.json': JSON.stringify({
353        name: 'my-project',
354        version: '1.0.0',
355      }),
356    },
357  })
358
359  npm.config.find = () => 'default'
360
361  await link.exec(['@myscope/linked'])
362
363  t.equal(
364    require(resolve(prefix, 'package.json')).dependencies,
365    undefined,
366    'should not save to package.json upon linking'
367  )
368
369  t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg')
370})
371
372t.test('should not save link to package file', async t => {
373  const { link, prefix } = await mockLink(t, {
374    globalPrefixDir: {
375      node_modules: {
376        '@myscope': {
377          linked: t.fixture('symlink', '../../../other/scoped-linked'),
378        },
379      },
380    },
381    otherDirs: {
382      'scoped-linked': {
383        'package.json': JSON.stringify({
384          name: '@myscope/linked',
385          version: '1.0.0',
386        }),
387      },
388    },
389    prefixDir: {
390      'package.json': JSON.stringify({
391        name: 'my-project',
392        version: '1.0.0',
393      }),
394    },
395    config: { save: false },
396  })
397
398  await link.exec(['@myscope/linked'])
399  t.match(
400    require(resolve(prefix, 'package.json')).dependencies,
401    undefined,
402    'should not save to package.json upon linking'
403  )
404})
405
406t.test('should not prune dependencies when linking packages', async t => {
407  const { link, prefix } = await mockLink(t, {
408    globalPrefixDir: {
409      node_modules: {
410        linked: t.fixture('symlink', '../../other/linked'),
411      },
412    },
413    otherDirs: {
414      linked: {
415        'package.json': JSON.stringify({
416          name: 'linked',
417          version: '1.0.0',
418        }),
419      },
420    },
421    prefixDir: {
422      node_modules: {
423        foo: {
424          'package.json': JSON.stringify({ name: 'foo', version: '1.0.0' }),
425        },
426      },
427      'package.json': JSON.stringify({
428        name: 'my-project',
429        version: '1.0.0',
430      }),
431    },
432  })
433
434  await link.exec(['linked'])
435
436  t.ok(
437    fs.statSync(resolve(prefix, 'node_modules/foo')),
438    'should not prune any extraneous dep when running npm link'
439  )
440})
441
442t.test('completion', async t => {
443  const { link } = await mockLink(t, {
444    globalPrefixDir: {
445      node_modules: {
446        foo: {},
447        bar: {},
448        lorem: {},
449        ipsum: {},
450      },
451    },
452  })
453
454  const words = await link.completion({})
455
456  t.same(
457    words,
458    ['bar', 'foo', 'ipsum', 'lorem'],
459    'should list all package names available in globalDir'
460  )
461})
462
463t.test('--global option', async t => {
464  const { link } = await mockLink(t, {
465    config: { global: true },
466  })
467  await t.rejects(
468    link.exec([]),
469    /link should never be --global/,
470    'should throw an useful error'
471  )
472})
473
474t.test('hash character in working directory path', async t => {
475  const { link, printLinks } = await mockLink(t, {
476    globalPrefixDir: {
477      node_modules: {
478        a: {
479          'package.json': JSON.stringify({
480            name: 'a',
481            version: '1.0.0',
482          }),
483        },
484      },
485    },
486    otherDirs: {
487      'i_like_#_in_my_paths': {
488        'test-pkg-link': {
489          'package.json': JSON.stringify({
490            name: 'test-pkg-link',
491            version: '1.0.0',
492          }),
493        },
494      },
495    },
496    chdir: ({ other }) => join(other, 'i_like_#_in_my_paths', 'test-pkg-link'),
497  })
498  await link.exec([])
499
500  t.matchSnapshot(await printLinks({ global: true }),
501    'should create a global link to current pkg, even within path with hash')
502})
503
504t.test('test linked installed as symlinks', async t => {
505  const { link, prefix, printLinks } = await mockLink(t, {
506    otherDirs: {
507      mylink: {
508        'package.json': JSON.stringify({
509          name: 'mylink',
510          version: '1.0.0',
511        }),
512      },
513    },
514  })
515
516  await link.exec([
517    join('file:../other/mylink'),
518  ])
519
520  t.ok(fs.lstatSync(join(prefix, 'node_modules', 'mylink')).isSymbolicLink(),
521    'linked path should by symbolic link'
522  )
523
524  t.matchSnapshot(await printLinks(), 'linked package should not be installed')
525})
526