• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1const t = require('tap')
2const MockRegistry = require('@npmcli/mock-registry')
3const _mockNpm = require('../../fixtures/mock-npm')
4const { cleanCwd } = require('../../fixtures/clean-snapshot')
5
6t.cleanSnapshot = (str) => cleanCwd(str)
7
8const packument = spec => {
9  const mocks = {
10    cat: {
11      name: 'cat',
12      'dist-tags': {
13        latest: '1.0.1',
14      },
15      versions: {
16        '1.0.1': {
17          version: '1.0.1',
18          dependencies: {
19            dog: '2.0.0',
20          },
21        },
22      },
23    },
24    chai: {
25      name: 'chai',
26      'dist-tags': {
27        latest: '1.0.1',
28      },
29      versions: {
30        '1.0.1': {
31          version: '1.0.1',
32        },
33      },
34    },
35    dog: {
36      name: 'dog',
37      'dist-tags': {
38        latest: '2.0.0',
39      },
40      versions: {
41        '1.0.1': {
42          version: '1.0.1',
43        },
44        '2.0.0': {
45          version: '2.0.0',
46        },
47      },
48    },
49    theta: {
50      name: 'theta',
51      'dist-tags': {
52        latest: '1.0.1',
53      },
54      versions: {
55        '1.0.1': {
56          version: '1.0.1',
57        },
58      },
59    },
60  }
61
62  if (spec.name === 'eta') {
63    throw new Error('There is an error with this package.')
64  }
65
66  if (!mocks[spec.name]) {
67    const err = new Error()
68    err.code = 'E404'
69    throw err
70  }
71
72  return mocks[spec.name]
73}
74
75const fixtures = {
76  global: {
77    node_modules: {
78      cat: {
79        'package.json': JSON.stringify({
80          name: 'cat',
81          version: '1.0.0',
82        }, null, 2),
83      },
84    },
85  },
86  local: {
87    'package.json': JSON.stringify({
88      name: 'delta',
89      version: '1.0.0',
90      dependencies: {
91        cat: '^1.0.0',
92        dog: '^1.0.0',
93        theta: '^1.0.0',
94      },
95      devDependencies: {
96        zeta: '^1.0.0',
97      },
98      optionalDependencies: {
99        lorem: '^1.0.0',
100      },
101      peerDependencies: {
102        chai: '^1.0.0',
103      },
104    }, null, 2),
105    node_modules: {
106      cat: {
107        'package.json': JSON.stringify({
108          name: 'cat',
109          version: '1.0.0',
110          dependencies: {
111            dog: '2.0.0',
112          },
113        }, null, 2),
114        node_modules: {
115          dog: {
116            'package.json': JSON.stringify({
117              name: 'dog',
118              version: '2.0.0',
119            }, null, 2),
120          },
121        },
122      },
123      chai: {
124        'package.json': JSON.stringify({
125          name: 'chai',
126          version: '1.0.0',
127        }, null, 2),
128      },
129      dog: {
130        'package.json': JSON.stringify({
131          name: 'dog',
132          version: '1.0.1',
133        }, null, 2),
134      },
135      zeta: {
136        'package.json': JSON.stringify({
137          name: 'zeta',
138          version: '1.0.0',
139        }, null, 2),
140      },
141    },
142  },
143  workspaces: {
144    'package.json': JSON.stringify({
145      name: 'workspaces-project',
146      version: '1.0.0',
147      workspaces: ['packages/*'],
148      dependencies: {
149        dog: '^1.0.0',
150      },
151    }),
152    node_modules: {
153      a: t.fixture('symlink', '../packages/a'),
154      b: t.fixture('symlink', '../packages/b'),
155      c: t.fixture('symlink', '../packages/c'),
156      cat: {
157        'package.json': JSON.stringify({
158          name: 'cat',
159          version: '1.0.0',
160          dependencies: {
161            dog: '2.0.0',
162          },
163        }),
164        node_modules: {
165          dog: {
166            'package.json': JSON.stringify({
167              name: 'dog',
168              version: '2.0.0',
169            }),
170          },
171        },
172      },
173      chai: {
174        'package.json': JSON.stringify({
175          name: 'chai',
176          version: '1.0.0',
177        }),
178      },
179      dog: {
180        'package.json': JSON.stringify({
181          name: 'dog',
182          version: '1.0.1',
183        }),
184      },
185      foo: {
186        'package.json': JSON.stringify({
187          name: 'foo',
188          version: '1.0.0',
189          dependencies: {
190            chai: '^1.0.0',
191          },
192        }),
193      },
194      zeta: {
195        'package.json': JSON.stringify({
196          name: 'zeta',
197          version: '1.0.0',
198        }),
199      },
200    },
201    packages: {
202      a: {
203        'package.json': JSON.stringify({
204          name: 'a',
205          version: '1.0.0',
206          dependencies: {
207            b: '^1.0.0',
208            cat: '^1.0.0',
209            foo: '^1.0.0',
210          },
211        }),
212      },
213      b: {
214        'package.json': JSON.stringify({
215          name: 'b',
216          version: '1.0.0',
217          dependencies: {
218            zeta: '^1.0.0',
219          },
220        }),
221      },
222      c: {
223        'package.json': JSON.stringify({
224          name: 'c',
225          version: '1.0.0',
226          dependencies: {
227            theta: '^1.0.0',
228          },
229        }),
230      },
231    },
232  },
233}
234
235const mockNpm = async (t, { prefixDir, ...opts } = {}) => {
236  const res = await _mockNpm(t, {
237    command: 'outdated',
238    mocks: {
239      pacote: {
240        packument,
241      },
242    },
243    ...opts,
244    prefixDir,
245  })
246
247  // this is not currently used, but ensures that no requests are
248  // hitting the registry.
249  // XXX: the pacote mock should be replaced with mock registry calls
250  const registry = new MockRegistry({
251    tap: t,
252    registry: res.npm.config.get('registry'),
253    strict: true,
254  })
255
256  return {
257    ...res,
258    registry,
259  }
260}
261
262t.test('should display outdated deps', async t => {
263  await t.test('outdated global', async t => {
264    const { outdated, joinedOutput } = await mockNpm(t, {
265      globalPrefixDir: fixtures.global,
266      config: { global: true },
267    })
268    await outdated.exec([])
269    t.equal(process.exitCode, 1)
270    t.matchSnapshot(joinedOutput())
271  })
272
273  await t.test('outdated', async t => {
274    const { outdated, joinedOutput } = await mockNpm(t, {
275      prefixDir: fixtures.local,
276      config: {
277        color: 'always',
278      },
279    })
280    await outdated.exec([])
281    t.equal(process.exitCode, 1)
282    t.matchSnapshot(joinedOutput())
283  })
284
285  await t.test('outdated --omit=dev', async t => {
286    const { outdated, joinedOutput } = await mockNpm(t, {
287      prefixDir: fixtures.local,
288      config: {
289        omit: ['dev'],
290        color: 'always',
291      },
292    })
293    await outdated.exec([])
294    t.equal(process.exitCode, 1)
295    t.matchSnapshot(joinedOutput())
296  })
297
298  await t.test('outdated --omit=dev --omit=peer', async t => {
299    const { outdated, joinedOutput } = await mockNpm(t, {
300      prefixDir: fixtures.local,
301      config: {
302        omit: ['dev', 'peer'],
303        color: 'always',
304      },
305    })
306    await outdated.exec([])
307    t.equal(process.exitCode, 1)
308    t.matchSnapshot(joinedOutput())
309  })
310
311  await t.test('outdated --omit=prod', async t => {
312    const { outdated, joinedOutput } = await mockNpm(t, {
313      prefixDir: fixtures.local,
314      config: {
315        omit: ['prod'],
316        color: 'always',
317      },
318    })
319    await outdated.exec([])
320    t.equal(process.exitCode, 1)
321    t.matchSnapshot(joinedOutput())
322  })
323
324  await t.test('outdated --long', async t => {
325    const { outdated, joinedOutput } = await mockNpm(t, {
326      prefixDir: fixtures.local,
327      config: {
328        long: true,
329      },
330    })
331    await outdated.exec([])
332    t.equal(process.exitCode, 1)
333    t.matchSnapshot(joinedOutput())
334  })
335
336  await t.test('outdated --json', async t => {
337    const { outdated, joinedOutput } = await mockNpm(t, {
338      prefixDir: fixtures.local,
339      config: {
340        json: true,
341      },
342    })
343    await outdated.exec([])
344    t.equal(process.exitCode, 1)
345    t.matchSnapshot(joinedOutput())
346  })
347
348  await t.test('outdated --json --long', async t => {
349    const { outdated, joinedOutput } = await mockNpm(t, {
350      prefixDir: fixtures.local,
351      config: {
352        json: true,
353        long: true,
354      },
355    })
356    await outdated.exec([])
357    t.equal(process.exitCode, 1)
358    t.matchSnapshot(joinedOutput())
359  })
360
361  await t.test('outdated --parseable', async t => {
362    const { outdated, joinedOutput } = await mockNpm(t, {
363      prefixDir: fixtures.local,
364      config: {
365        parseable: true,
366      },
367    })
368    await outdated.exec([])
369    t.equal(process.exitCode, 1)
370    t.matchSnapshot(joinedOutput())
371  })
372
373  await t.test('outdated --parseable --long', async t => {
374    const { outdated, joinedOutput } = await mockNpm(t, {
375      prefixDir: fixtures.local,
376      config: {
377        parseable: true,
378        long: true,
379      },
380    })
381    await outdated.exec([])
382    t.equal(process.exitCode, 1)
383    t.matchSnapshot(joinedOutput())
384  })
385
386  await t.test('outdated --all', async t => {
387    const { outdated, joinedOutput } = await mockNpm(t, {
388      prefixDir: fixtures.local,
389      config: {
390        all: true,
391      },
392    })
393    await outdated.exec([])
394    t.equal(process.exitCode, 1)
395    t.matchSnapshot(joinedOutput())
396  })
397
398  await t.test('outdated specific dep', async t => {
399    const { outdated, joinedOutput } = await mockNpm(t, {
400      prefixDir: fixtures.local,
401    })
402    await outdated.exec(['cat'])
403    t.equal(process.exitCode, 1)
404    t.matchSnapshot(joinedOutput())
405  })
406})
407
408t.test('should return if no outdated deps', async t => {
409  const testDir = {
410    'package.json': JSON.stringify({
411      name: 'delta',
412      version: '1.0.0',
413      dependencies: {
414        cat: '^1.0.0',
415      },
416    }, null, 2),
417    node_modules: {
418      cat: {
419        'package.json': JSON.stringify({
420          name: 'cat',
421          version: '1.0.1',
422        }, null, 2),
423      },
424    },
425  }
426
427  const { outdated, joinedOutput } = await mockNpm(t, {
428    prefixDir: testDir,
429
430  })
431  await outdated.exec([])
432  t.equal(joinedOutput(), '', 'no logs')
433})
434
435t.test('throws if error with a dep', async t => {
436  const testDir = {
437    'package.json': JSON.stringify({
438      name: 'delta',
439      version: '1.0.0',
440      dependencies: {
441        eta: '^1.0.0',
442      },
443    }, null, 2),
444    node_modules: {
445      eta: {
446        'package.json': JSON.stringify({
447          name: 'eta',
448          version: '1.0.1',
449        }, null, 2),
450      },
451    },
452  }
453
454  const { outdated } = await mockNpm(t, {
455    prefixDir: testDir,
456  })
457
458  await t.rejects(outdated.exec([]), 'There is an error with this package.')
459})
460
461t.test('should skip missing non-prod deps', async t => {
462  const testDir = {
463    'package.json': JSON.stringify({
464      name: 'delta',
465      version: '1.0.0',
466      devDependencies: {
467        chai: '^1.0.0',
468      },
469    }, null, 2),
470    node_modules: {},
471  }
472
473  const { outdated, joinedOutput } = await mockNpm(t, {
474    prefixDir: testDir,
475  })
476
477  await outdated.exec([])
478
479  t.equal(joinedOutput(), '', 'no logs')
480})
481
482t.test('should skip invalid pkg ranges', async t => {
483  const testDir = {
484    'package.json': JSON.stringify({
485      name: 'delta',
486      version: '1.0.0',
487      dependencies: {
488        cat: '>=^2',
489      },
490    }, null, 2),
491    node_modules: {
492      cat: {
493        'package.json': JSON.stringify({
494          name: 'cat',
495          version: '1.0.0',
496        }, null, 2),
497      },
498    },
499  }
500
501  const { outdated, joinedOutput } = await mockNpm(t, {
502    prefixDir: testDir,
503  })
504  await outdated.exec([])
505  t.equal(joinedOutput(), '', 'no logs')
506})
507
508t.test('should skip git specs', async t => {
509  const testDir = {
510    'package.json': JSON.stringify({
511      name: 'delta',
512      version: '1.0.0',
513      dependencies: {
514        cat: 'github:username/foo',
515      },
516    }, null, 2),
517    node_modules: {
518      cat: {
519        'package.json': JSON.stringify({
520          name: 'cat',
521          version: '1.0.0',
522        }, null, 2),
523      },
524    },
525  }
526
527  const { outdated, joinedOutput } = await mockNpm(t, {
528    prefixDir: testDir,
529  })
530  await outdated.exec([])
531  t.equal(joinedOutput(), '', 'no logs')
532})
533
534t.test('workspaces', async t => {
535  const mockWorkspaces = async (t, { exitCode = 1, ...config } = {}) => {
536    const { outdated, joinedOutput } = await mockNpm(t, {
537      prefixDir: fixtures.workspaces,
538      config,
539    })
540
541    await outdated.exec([])
542
543    t.matchSnapshot(joinedOutput(), 'output')
544    t.equal(process.exitCode, exitCode ?? undefined)
545  }
546
547  await t.test('should display ws outdated deps human output', t =>
548    mockWorkspaces(t))
549
550  // TODO: This should display dog, but doesn't because arborist filters
551  // workspace deps even if they're also root deps
552  // This will be fixed in a future arborist version
553  await t.test('should display only root outdated when ws disabled', t =>
554    mockWorkspaces(t, { workspaces: false, exitCode: null }))
555
556  await t.test('should display ws outdated deps json output', t =>
557    mockWorkspaces(t, { json: true }))
558
559  await t.test('should display ws outdated deps parseable output', t =>
560    mockWorkspaces(t, { parseable: true }))
561
562  await t.test('should display all dependencies', t =>
563    mockWorkspaces(t, { all: true }))
564
565  await t.test('should highlight ws in dependend by section', t =>
566    mockWorkspaces(t, { color: 'always' }))
567
568  await t.test('should display results filtered by ws', t =>
569    mockWorkspaces(t, { workspace: 'a' }))
570
571  await t.test('should display json results filtered by ws', t =>
572    mockWorkspaces(t, { json: true, workspace: 'a' }))
573
574  await t.test('should display parseable results filtered by ws', t =>
575    mockWorkspaces(t, { parseable: true, workspace: 'a' }))
576
577  await t.test('should display nested deps when filtering by ws and using --all', t =>
578    mockWorkspaces(t, { all: true, workspace: 'a' }))
579
580  await t.test('should display no results if ws has no deps to display', t =>
581    mockWorkspaces(t, { workspace: 'b', exitCode: null }))
582
583  await t.test('should display missing deps when filtering by ws', t =>
584    mockWorkspaces(t, { workspace: 'c', exitCode: 1 }))
585})
586
587t.test('aliases', async t => {
588  const testDir = {
589    'package.json': JSON.stringify({
590      name: 'display-aliases',
591      version: '1.0.0',
592      dependencies: {
593        cat: 'npm:dog@latest',
594      },
595    }),
596    node_modules: {
597      cat: {
598        'package.json': JSON.stringify({
599          name: 'dog',
600          version: '1.0.0',
601        }),
602      },
603    },
604  }
605
606  const { outdated, joinedOutput } = await mockNpm(t, {
607    prefixDir: testDir,
608  })
609  await outdated.exec([])
610
611  t.matchSnapshot(joinedOutput(), 'should display aliased outdated dep output')
612  t.equal(process.exitCode, 1)
613})
614