@@ -14,13 +14,14 @@ const readShrinkwrap = require('./install/read-shrinkwrap.js')
1414const mutateIntoLogicalTree = require ( './install/mutate-into-logical-tree.js' )
1515const output = require ( './utils/output.js' )
1616const openUrl = require ( './utils/open-url.js' )
17- const { getFundingInfo, retrieveFunding, validFundingUrl } = require ( './utils/funding.js' )
17+ const { getFundingInfo, retrieveFunding, validFundingField , flatCacheSymbol } = require ( './utils/funding.js' )
1818
1919const FundConfig = figgyPudding ( {
2020 browser : { } , // used by ./utils/open-url
2121 global : { } ,
2222 json : { } ,
23- unicode : { }
23+ unicode : { } ,
24+ which : { }
2425} )
2526
2627module . exports = fundCmd
@@ -29,7 +30,7 @@ const usage = require('./utils/usage')
2930fundCmd . usage = usage (
3031 'fund' ,
3132 'npm fund [--json]' ,
32- 'npm fund [--browser] [[<@scope>/]<pkg>'
33+ 'npm fund [--browser] [[<@scope>/]<pkg> [--which=<fundingSourceNumber>] '
3334)
3435
3536fundCmd . completion = function ( opts , cb ) {
@@ -52,96 +53,52 @@ function printJSON (fundingInfo) {
5253// level possible, in that process they also carry their dependencies along
5354// with them, moving those up in the visual tree
5455function printHuman ( fundingInfo , opts ) {
55- // mapping logic that keeps track of seen items in order to be able
56- // to push all other items from the same type/url in the same place
57- const seen = new Map ( )
56+ const flatCache = fundingInfo [ flatCacheSymbol ]
5857
59- function seenKey ( { type, url } = { } ) {
60- return url ? String ( type ) + String ( url ) : null
61- }
62-
63- function setStackedItem ( funding , result ) {
64- const key = seenKey ( funding )
65- if ( key && ! seen . has ( key ) ) seen . set ( key , result )
66- }
58+ const { name, version } = fundingInfo
59+ const printableVersion = version ? `@${ version } ` : ''
6760
68- function retrieveStackedItem ( funding ) {
69- const key = seenKey ( funding )
70- if ( key && seen . has ( key ) ) return seen . get ( key )
71- }
61+ const items = Object . keys ( flatCache ) . map ( ( url ) => {
62+ const deps = flatCache [ url ]
7263
73- // ---
74-
75- const getFundingItems = ( fundingItems ) =>
76- Object . keys ( fundingItems || { } ) . map ( ( fundingItemName ) => {
77- // first-level loop, prepare the pretty-printed formatted data
78- const fundingItem = fundingItems [ fundingItemName ]
79- const { version, funding } = fundingItem
80- const { type, url } = funding || { }
64+ const packages = deps . map ( ( dep ) => {
65+ const { name, version } = dep
8166
8267 const printableVersion = version ? `@${ version } ` : ''
83- const printableType = type && { label : `type: ${ funding . type } ` }
84- const printableUrl = url && { label : `url: ${ funding . url } ` }
85- const result = {
86- fundingItem,
87- label : fundingItemName + printableVersion ,
88- nodes : [ ]
89- }
90-
91- if ( printableType ) {
92- result . nodes . push ( printableType )
93- }
94-
95- if ( printableUrl ) {
96- result . nodes . push ( printableUrl )
97- }
98-
99- setStackedItem ( funding , result )
100-
101- return result
102- } ) . reduce ( ( res , result ) => {
103- // recurse and exclude nodes that are going to be stacked together
104- const { fundingItem } = result
105- const { dependencies, funding } = fundingItem
106- const items = getFundingItems ( dependencies )
107- const stackedResult = retrieveStackedItem ( funding )
108- items . forEach ( i => result . nodes . push ( i ) )
109-
110- if ( stackedResult && stackedResult !== result ) {
111- stackedResult . label += `, ${ result . label } `
112- items . forEach ( i => stackedResult . nodes . push ( i ) )
113- return res
114- }
115-
116- res . push ( result )
117-
118- return res
119- } , [ ] )
120-
121- const [ result ] = getFundingItems ( {
122- [ fundingInfo . name ] : {
123- dependencies : fundingInfo . dependencies ,
124- funding : fundingInfo . funding ,
125- version : fundingInfo . version
68+ return `${ name } ${ printableVersion } `
69+ } )
70+
71+ return {
72+ label : url ,
73+ nodes : [ packages . join ( ', ' ) ]
12674 }
12775 } )
12876
129- return archy ( result , '' , { unicode : opts . unicode } )
77+ return archy ( { label : ` ${ name } ${ printableVersion } ` , nodes : items } , '' , { unicode : opts . unicode } )
13078}
13179
132- function openFundingUrl ( packageName , cb ) {
80+ function openFundingUrl ( packageName , fundingSourceNumber , cb ) {
13381 function getUrlAndOpen ( packageMetadata ) {
13482 const { funding } = packageMetadata
135- const { type, url } = retrieveFunding ( funding ) || { }
136- const noFundingError =
137- new Error ( `No funding method available for: ${ packageName } ` )
138- noFundingError . code = 'ENOFUND'
139- const typePrefix = type ? `${ type } funding` : 'Funding'
140- const msg = `${ typePrefix } available at the following URL`
141-
142- if ( validFundingUrl ( funding ) ) {
83+ const validSources = [ ] . concat ( retrieveFunding ( funding ) ) . filter ( validFundingField )
84+
85+ if ( validSources . length === 1 || ( fundingSourceNumber > 0 && fundingSourceNumber <= validSources . length ) ) {
86+ const { type, url } = validSources [ fundingSourceNumber ? fundingSourceNumber - 1 : 0 ]
87+ const typePrefix = type ? `${ type } funding` : 'Funding'
88+ const msg = `${ typePrefix } available at the following URL`
14389 openUrl ( url , msg , cb )
90+ } else if ( ! ( fundingSourceNumber >= 1 ) ) {
91+ validSources . forEach ( ( { type, url } , i ) => {
92+ const typePrefix = type ? `${ type } funding` : 'Funding'
93+ const msg = `${ typePrefix } available at the following URL`
94+ console . log ( `${ i + 1 } : ${ msg } : ${ url } ` )
95+ } )
96+ console . log ( 'Run `npm fund [<@scope>/]<pkg> --which=1`, for example, to open the first funding URL listed in that package' )
97+ cb ( )
14498 } else {
99+ const noFundingError = new Error ( `No valid funding method available for: ${ packageName } ` )
100+ noFundingError . code = 'ENOFUND'
101+
145102 throw noFundingError
146103 }
147104 }
@@ -161,15 +118,24 @@ function fundCmd (args, cb) {
161118 const opts = FundConfig ( npmConfig ( ) )
162119 const dir = path . resolve ( npm . dir , '..' )
163120 const packageName = args [ 0 ]
121+ const numberArg = opts . which
122+
123+ const fundingSourceNumber = numberArg && parseInt ( numberArg , 10 )
124+
125+ if ( numberArg !== undefined && ( String ( fundingSourceNumber ) !== numberArg || fundingSourceNumber < 1 ) ) {
126+ const err = new Error ( '`npm fund [<@scope>/]<pkg> [--which=fundingSourceNumber]` must be given a positive integer' )
127+ err . code = 'EFUNDNUMBER'
128+ throw err
129+ }
164130
165131 if ( opts . global ) {
166- const err = new Error ( '`npm fund` does not support globals ' )
132+ const err = new Error ( '`npm fund` does not support global packages ' )
167133 err . code = 'EFUNDGLOBAL'
168134 throw err
169135 }
170136
171137 if ( packageName ) {
172- openFundingUrl ( packageName , cb )
138+ openFundingUrl ( packageName , fundingSourceNumber , cb )
173139 return
174140 }
175141
0 commit comments