• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1const t = require('tap')
2const mockNpm = require('../../fixtures/mock-npm')
3
4const version = '1.0.0'
5
6const funding = {
7  type: 'individual',
8  url: 'http://example.com/donate',
9}
10
11const maintainerOwnsAllDeps = {
12  'package.json': JSON.stringify({
13    name: 'maintainer-owns-all-deps',
14    version,
15    funding,
16    dependencies: {
17      'dep-foo': '*',
18      'dep-bar': '*',
19    },
20  }),
21  node_modules: {
22    'dep-foo': {
23      'package.json': JSON.stringify({
24        name: 'dep-foo',
25        version,
26        funding,
27        dependencies: {
28          'dep-sub-foo': '*',
29        },
30      }),
31      node_modules: {
32        'dep-sub-foo': {
33          'package.json': JSON.stringify({
34            name: 'dep-sub-foo',
35            version,
36            funding,
37          }),
38        },
39      },
40    },
41    'dep-bar': {
42      'package.json': JSON.stringify({
43        name: 'dep-bar',
44        version,
45        funding,
46      }),
47    },
48  },
49}
50
51const nestedNoFundingPackages = {
52  'package.json': JSON.stringify({
53    name: 'nested-no-funding-packages',
54    version,
55    dependencies: {
56      foo: '*',
57    },
58    devDependencies: {
59      lorem: '*',
60    },
61  }),
62  node_modules: {
63    foo: {
64      'package.json': JSON.stringify({
65        name: 'foo',
66        version,
67        dependencies: {
68          bar: '*',
69        },
70      }),
71      node_modules: {
72        bar: {
73          'package.json': JSON.stringify({
74            name: 'bar',
75            version,
76            funding,
77          }),
78          node_modules: {
79            'sub-bar': {
80              'package.json': JSON.stringify({
81                name: 'sub-bar',
82                version,
83                funding: 'https://example.com/sponsor',
84              }),
85            },
86          },
87        },
88      },
89    },
90    lorem: {
91      'package.json': JSON.stringify({
92        name: 'lorem',
93        version,
94        funding: {
95          url: 'https://example.com/lorem',
96        },
97      }),
98    },
99  },
100}
101
102const nestedMultipleFundingPackages = {
103  'package.json': JSON.stringify({
104    name: 'nested-multiple-funding-packages',
105    version,
106    funding: ['https://one.example.com', 'https://two.example.com'],
107    dependencies: {
108      foo: '*',
109    },
110    devDependencies: {
111      bar: '*',
112    },
113  }),
114  node_modules: {
115    foo: {
116      'package.json': JSON.stringify({
117        name: 'foo',
118        version,
119        funding: [
120          'http://example.com',
121          { url: 'http://sponsors.example.com/me' },
122          'http://collective.example.com',
123        ],
124      }),
125    },
126    bar: {
127      'package.json': JSON.stringify({
128        name: 'bar',
129        version,
130        funding: ['http://collective.example.com', { url: 'http://sponsors.example.com/you' }],
131      }),
132    },
133  },
134}
135
136const conflictingFundingPackages = {
137  'package.json': JSON.stringify({
138    name: 'conflicting-funding-packages',
139    version,
140    dependencies: {
141      foo: '1.0.0',
142    },
143    devDependencies: {
144      bar: '1.0.0',
145    },
146  }),
147  node_modules: {
148    foo: {
149      'package.json': JSON.stringify({
150        name: 'foo',
151        version: '1.0.0',
152        funding: 'http://example.com/1',
153      }),
154    },
155    bar: {
156      node_modules: {
157        foo: {
158          'package.json': JSON.stringify({
159            name: 'foo',
160            version: '2.0.0',
161            funding: 'http://example.com/2',
162          }),
163        },
164      },
165      'package.json': JSON.stringify({
166        name: 'bar',
167        version: '1.0.0',
168        dependencies: {
169          foo: '2.0.0',
170        },
171      }),
172    },
173  },
174}
175
176const setup = async (t, { openUrl, ...opts } = {}) => {
177  const openedUrls = []
178
179  const res = await mockNpm(t, {
180    ...opts,
181    mocks: {
182      '@npmcli/promise-spawn': { open: openUrl || (async url => openedUrls.push(url)) },
183      pacote: {
184        manifest: arg =>
185          arg.name === 'ntl'
186            ? Promise.resolve({ funding: 'http://example.com/pacote' })
187            : Promise.reject(new Error('ERROR')),
188      },
189      ...opts.mocks,
190    },
191  })
192
193  return {
194    ...res,
195    openedUrls: () => openedUrls,
196    fund: (...args) => res.npm.exec('fund', args),
197  }
198}
199
200t.test('fund with no package containing funding', async t => {
201  const { fund, joinedOutput } = await setup(t, {
202    prefixDir: {
203      'package.json': JSON.stringify({
204        name: 'no-funding-package',
205        version: '0.0.0',
206      }),
207    },
208    config: {},
209  })
210
211  await fund()
212  t.matchSnapshot(joinedOutput(), 'should print empty funding info')
213})
214
215t.test('fund in which same maintainer owns all its deps', async t => {
216  const { fund, joinedOutput } = await setup(t, {
217    prefixDir: maintainerOwnsAllDeps,
218    config: {},
219  })
220
221  await fund()
222  t.matchSnapshot(joinedOutput(), 'should print stack packages together')
223})
224
225t.test('fund in which same maintainer owns all its deps, using --json option', async t => {
226  const { fund, joinedOutput } = await setup(t, {
227    prefixDir: maintainerOwnsAllDeps,
228    config: { json: true },
229  })
230
231  await fund()
232  t.same(
233    JSON.parse(joinedOutput()),
234    {
235      length: 3,
236      name: 'maintainer-owns-all-deps',
237      version: '1.0.0',
238      funding: { type: 'individual', url: 'http://example.com/donate' },
239      dependencies: {
240        'dep-bar': {
241          version: '1.0.0',
242          funding: { type: 'individual', url: 'http://example.com/donate' },
243        },
244        'dep-foo': {
245          version: '1.0.0',
246          funding: { type: 'individual', url: 'http://example.com/donate' },
247          dependencies: {
248            'dep-sub-foo': {
249              version: '1.0.0',
250              funding: { type: 'individual', url: 'http://example.com/donate' },
251            },
252          },
253        },
254      },
255    },
256    'should print stack packages together'
257  )
258})
259
260t.test('fund containing multi-level nested deps with no funding', async t => {
261  const { fund, joinedOutput } = await setup(t, {
262    prefixDir: nestedNoFundingPackages,
263    config: {},
264  })
265
266  await fund()
267  t.matchSnapshot(joinedOutput(), 'should omit dependencies with no funding declared')
268})
269
270t.test('fund containing multi-level nested deps with no funding, using --json option', async t => {
271  const { fund, joinedOutput } = await setup(t, {
272    prefixDir: nestedNoFundingPackages,
273    config: { json: true },
274  })
275
276  await fund()
277  t.same(
278    JSON.parse(joinedOutput()),
279    {
280      length: 2,
281      name: 'nested-no-funding-packages',
282      version: '1.0.0',
283      dependencies: {
284        lorem: {
285          version: '1.0.0',
286          funding: { url: 'https://example.com/lorem' },
287        },
288        bar: {
289          version: '1.0.0',
290          funding: { type: 'individual', url: 'http://example.com/donate' },
291        },
292      },
293    },
294    'should omit dependencies with no funding declared in json output'
295  )
296})
297
298t.test('fund containing multi-level nested deps with no funding, using --json option', async t => {
299  const { fund, joinedOutput } = await setup(t, {
300    prefixDir: nestedMultipleFundingPackages,
301    config: { json: true },
302  })
303
304  await fund()
305  t.same(
306    JSON.parse(joinedOutput()),
307    {
308      length: 2,
309      name: 'nested-multiple-funding-packages',
310      version: '1.0.0',
311      funding: [
312        {
313          url: 'https://one.example.com',
314        },
315        {
316          url: 'https://two.example.com',
317        },
318      ],
319      dependencies: {
320        bar: {
321          version: '1.0.0',
322          funding: [
323            {
324              url: 'http://collective.example.com',
325            },
326            {
327              url: 'http://sponsors.example.com/you',
328            },
329          ],
330        },
331        foo: {
332          version: '1.0.0',
333          funding: [
334            {
335              url: 'http://example.com',
336            },
337            {
338              url: 'http://sponsors.example.com/me',
339            },
340            {
341              url: 'http://collective.example.com',
342            },
343          ],
344        },
345      },
346    },
347    'should list multiple funding entries in json output'
348  )
349})
350
351t.test('fund does not support global', async t => {
352  const { fund } = await setup(t, {
353    config: { global: true },
354  })
355
356  await t.rejects(fund(), { code: 'EFUNDGLOBAL' }, 'should throw EFUNDGLOBAL error')
357})
358
359t.test('fund using package argument', async t => {
360  const { fund, openedUrls, joinedOutput } = await setup(t, {
361    prefixDir: maintainerOwnsAllDeps,
362    config: {},
363  })
364
365  await fund('.')
366  t.equal(joinedOutput(), '')
367  t.strictSame(openedUrls(), ['http://example.com/donate'], 'should open funding url')
368})
369
370t.test('fund does not support global, using --json option', async t => {
371  const { fund } = await setup(t, {
372    prefixDir: {},
373    config: { global: true, json: true },
374  })
375
376  await t.rejects(
377    fund(),
378    { code: 'EFUNDGLOBAL', message: '`npm fund` does not support global packages' },
379    'should use expected error msg'
380  )
381})
382
383t.test('fund using string shorthand', async t => {
384  const { fund, openedUrls } = await setup(t, {
385    prefixDir: {
386      'package.json': JSON.stringify({
387        name: 'funding-string-shorthand',
388        version: '0.0.0',
389        funding: 'https://example.com/sponsor',
390      }),
391    },
392    config: {},
393  })
394
395  await fund('.')
396  t.strictSame(openedUrls(), ['https://example.com/sponsor'], 'should open string-only url')
397})
398
399t.test('fund using nested packages with multiple sources', async t => {
400  const { fund, joinedOutput } = await setup(t, {
401    prefixDir: nestedMultipleFundingPackages,
402    config: {},
403  })
404
405  await fund('.')
406  t.matchSnapshot(joinedOutput(), 'should prompt with all available URLs')
407})
408
409t.test('fund using symlink ref', async t => {
410  const f = 'http://example.com/a'
411  const { fund, openedUrls } = await setup(t, {
412    prefixDir: {
413      'package.json': JSON.stringify({
414        name: 'using-symlink-ref',
415        version: '1.0.0',
416      }),
417      a: {
418        'package.json': JSON.stringify({
419          name: 'a',
420          version: '1.0.0',
421          funding: f,
422        }),
423      },
424      node_modules: {
425        a: t.fixture('symlink', '../a'),
426      },
427    },
428    config: {},
429  })
430
431  // using symlinked ref
432  await fund('./node_modules/a')
433  t.strictSame(openedUrls(), [f], 'should retrieve funding url from symlink')
434
435  // using target ref
436  await fund('./a')
437  t.strictSame(openedUrls(), [f, f], 'should retrieve funding url from symlink target')
438})
439
440t.test('fund using data from actual tree', async t => {
441  const { fund, openedUrls } = await setup(t, {
442    prefixDir: {
443      'package.json': JSON.stringify({
444        name: 'using-actual-tree',
445        version: '1.0.0',
446      }),
447      node_modules: {
448        a: {
449          'package.json': JSON.stringify({
450            name: 'a',
451            version: '1.0.0',
452            funding: 'http://example.com/a',
453          }),
454        },
455        b: {
456          'package.json': JSON.stringify({
457            name: 'a',
458            version: '1.0.0',
459            funding: 'http://example.com/b',
460          }),
461          node_modules: {
462            a: {
463              'package.json': JSON.stringify({
464                name: 'a',
465                version: '1.0.1',
466                funding: 'http://example.com/_AAA',
467              }),
468            },
469          },
470        },
471      },
472    },
473    config: {},
474  })
475
476  // using symlinked ref
477  await fund('a')
478  t.strictSame(
479    openedUrls(),
480    ['http://example.com/_AAA'],
481    'should retrieve fund info from actual tree, using greatest version found'
482  )
483})
484
485t.test('fund using nested packages with multiple sources, with a source number', async t => {
486  const { fund, openedUrls } = await setup(t, {
487    prefixDir: nestedMultipleFundingPackages,
488    config: { which: '1' },
489  })
490
491  await fund('.')
492  t.strictSame(openedUrls(), ['https://one.example.com'], 'should open the numbered URL')
493})
494
495t.test('fund using pkg name while having conflicting versions', async t => {
496  const { fund, openedUrls } = await setup(t, {
497    prefixDir: conflictingFundingPackages,
498    config: { which: '1' },
499  })
500
501  await fund('foo')
502  t.strictSame(openedUrls(), ['http://example.com/2'], 'should open greatest version')
503})
504
505t.test('fund using bad which value: index too high', async t => {
506  const { fund, joinedOutput } = await setup(t, {
507    prefixDir: nestedMultipleFundingPackages,
508    config: { which: '100' },
509  })
510
511  await fund('foo')
512  t.match(joinedOutput(), 'not a valid index')
513  t.matchSnapshot(joinedOutput(), 'should print message about invalid which')
514})
515
516t.test('fund using package argument with no browser, using --json option', async t => {
517  const { fund, openedUrls, joinedOutput } = await setup(t, {
518    prefixDir: maintainerOwnsAllDeps,
519    config: { json: true },
520  })
521
522  await fund('.')
523  t.equal(joinedOutput(), '', 'no output')
524  t.same(
525    openedUrls(),
526    ['http://example.com/donate'],
527    'should open funding url using json output'
528  )
529})
530
531t.test('fund using package info fetch from registry', async t => {
532  const { fund, openedUrls } = await setup(t, {
533    prefixDir: {},
534    config: {},
535  })
536
537  await fund('ntl')
538  t.match(
539    openedUrls(),
540    /http:\/\/example.com\/pacote/,
541    'should open funding url that was loaded from registry manifest'
542  )
543})
544
545t.test('fund tries to use package info fetch from registry but registry has nothing', async t => {
546  const { fund } = await setup(t, {
547    prefixDir: {},
548    config: {},
549  })
550
551  await t.rejects(
552    fund('foo'),
553    { code: 'ENOFUND', message: 'No valid funding method available for: foo' },
554    'should have no valid funding message'
555  )
556})
557
558t.test('fund but target module has no funding info', async t => {
559  const { fund } = await setup(t, {
560    prefixDir: nestedNoFundingPackages,
561    config: {},
562  })
563
564  await t.rejects(
565    fund('foo'),
566    { code: 'ENOFUND', message: 'No valid funding method available for: foo' },
567    'should have no valid funding message'
568  )
569})
570
571t.test('fund using bad which value', async t => {
572  const { fund } = await setup(t, {
573    prefixDir: nestedMultipleFundingPackages,
574    config: { which: '0' },
575  })
576
577  await t.rejects(
578    fund('bar'),
579    {
580      code: 'EFUNDNUMBER',
581      message: /must be given a positive integer/,
582    },
583    'should have bad which option error message'
584  )
585})
586
587t.test('fund pkg missing version number', async t => {
588  const { fund, joinedOutput } = await setup(t, {
589    prefixDir: {
590      'package.json': JSON.stringify({
591        name: 'foo',
592        funding: 'http://example.com/foo',
593      }),
594    },
595    config: {},
596  })
597
598  await fund()
599  t.matchSnapshot(joinedOutput(), 'should print name only')
600})
601
602t.test('fund a package throws on openUrl', async t => {
603  const { fund } = await setup(t, {
604    prefixDir: {
605      'package.json': JSON.stringify({
606        name: 'foo',
607        version: '1.0.0',
608        funding: 'http://npmjs.org',
609      }),
610    },
611    config: {},
612    openUrl: () => {
613      throw new Error('ERROR')
614    },
615  })
616
617  await t.rejects(fund('.'), { message: 'ERROR' }, 'should throw unknown error')
618})
619
620t.test('fund a package with type and multiple sources', async t => {
621  const { fund, joinedOutput } = await setup(t, {
622    prefixDir: {
623      'package.json': JSON.stringify({
624        name: 'foo',
625        funding: [
626          {
627            type: 'Foo',
628            url: 'http://example.com/foo',
629          },
630          {
631            type: 'Lorem',
632            url: 'http://example.com/foo-lorem',
633          },
634        ],
635      }),
636    },
637    config: {},
638  })
639
640  await fund('.')
641  t.matchSnapshot(joinedOutput(), 'should print prompt select message')
642})
643
644t.test('fund colors', async t => {
645  const { fund, joinedOutput } = await setup(t, {
646    prefixDir: {
647      'package.json': JSON.stringify({
648        name: 'test-fund-colors',
649        version: '1.0.0',
650        dependencies: {
651          a: '^1.0.0',
652          b: '^1.0.0',
653          c: '^1.0.0',
654        },
655      }),
656      node_modules: {
657        a: {
658          'package.json': JSON.stringify({
659            name: 'a',
660            version: '1.0.0',
661            funding: 'http://example.com/a',
662          }),
663        },
664        b: {
665          'package.json': JSON.stringify({
666            name: 'b',
667            version: '1.0.0',
668            funding: 'http://example.com/b',
669            dependencies: {
670              d: '^1.0.0',
671              e: '^1.0.0',
672            },
673          }),
674        },
675        c: {
676          'package.json': JSON.stringify({
677            name: 'c',
678            version: '1.0.0',
679            funding: 'http://example.com/b',
680          }),
681        },
682        d: {
683          'package.json': JSON.stringify({
684            name: 'd',
685            version: '1.0.0',
686            funding: 'http://example.com/d',
687          }),
688        },
689        e: {
690          'package.json': JSON.stringify({
691            name: 'e',
692            version: '1.0.0',
693            funding: 'http://example.com/e',
694          }),
695        },
696      },
697    },
698    config: { color: 'always' },
699  })
700
701  await fund()
702  t.matchSnapshot(joinedOutput(), 'should print output with color info')
703})
704
705t.test('sub dep with fund info and a parent with no funding info', async t => {
706  const { fund, joinedOutput } = await setup(t, {
707    prefixDir: {
708      'package.json': JSON.stringify({
709        name: 'test-multiple-funding-sources',
710        version: '1.0.0',
711        dependencies: {
712          a: '^1.0.0',
713          b: '^1.0.0',
714        },
715      }),
716      node_modules: {
717        a: {
718          'package.json': JSON.stringify({
719            name: 'a',
720            version: '1.0.0',
721            dependencies: {
722              c: '^1.0.0',
723            },
724          }),
725        },
726        b: {
727          'package.json': JSON.stringify({
728            name: 'b',
729            version: '1.0.0',
730            funding: 'http://example.com/b',
731          }),
732        },
733        c: {
734          'package.json': JSON.stringify({
735            name: 'c',
736            version: '1.0.0',
737            funding: ['http://example.com/c', 'http://example.com/c-other'],
738          }),
739        },
740      },
741    },
742    config: {},
743  })
744
745  await fund()
746  t.matchSnapshot(joinedOutput(), 'should nest sub dep as child of root')
747})
748
749t.test('workspaces', async t => {
750  const wsPrefixDir = {
751    'package.json': JSON.stringify({
752      name: 'workspaces-support',
753      version: '1.0.0',
754      workspaces: ['packages/*'],
755      dependencies: {
756        d: '^1.0.0',
757      },
758    }),
759    node_modules: {
760      a: t.fixture('symlink', '../packages/a'),
761      b: t.fixture('symlink', '../packages/b'),
762      c: {
763        'package.json': JSON.stringify({
764          name: 'c',
765          version: '1.0.0',
766          funding: ['http://example.com/c', 'http://example.com/c-other'],
767        }),
768      },
769      d: {
770        'package.json': JSON.stringify({
771          name: 'd',
772          version: '1.0.0',
773          funding: 'http://example.com/d',
774        }),
775      },
776    },
777    packages: {
778      a: {
779        'package.json': JSON.stringify({
780          name: 'a',
781          version: '1.0.0',
782          funding: 'https://example.com/a',
783          dependencies: {
784            c: '^1.0.0',
785          },
786        }),
787      },
788      b: {
789        'package.json': JSON.stringify({
790          name: 'b',
791          version: '1.0.0',
792          funding: 'http://example.com/b',
793          dependencies: {
794            d: '^1.0.0',
795          },
796        }),
797      },
798    },
799  }
800
801  t.test('filter funding info by a specific workspace name', async t => {
802    const { fund, joinedOutput } = await setup(t, {
803      prefixDir: wsPrefixDir,
804      config: {
805        workspace: 'a',
806      },
807    })
808
809    await fund()
810    t.matchSnapshot(joinedOutput(), 'should display only filtered workspace name and its deps')
811  })
812
813  t.test('filter funding info by a specific workspace path', async t => {
814    const { fund, joinedOutput } = await setup(t, {
815      prefixDir: wsPrefixDir,
816      config: {
817        workspace: './packages/a',
818      },
819    })
820
821    await fund()
822    t.matchSnapshot(joinedOutput(), 'should display only filtered workspace name and its deps')
823  })
824})
825